mainsequence 9.0.26__tar.gz → 9.0.27__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. {mainsequence-9.0.26/mainsequence.egg-info → mainsequence-9.0.27}/PKG-INFO +1 -1
  2. {mainsequence-9.0.26 → mainsequence-9.0.27}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +2 -4
  3. {mainsequence-9.0.26 → mainsequence-9.0.27}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +4 -4
  4. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/client/models_user.py +1 -44
  5. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/server/caller_assertions.py +10 -35
  6. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/server/fastapi.py +0 -2
  7. {mainsequence-9.0.26 → mainsequence-9.0.27/mainsequence.egg-info}/PKG-INFO +1 -1
  8. {mainsequence-9.0.26 → mainsequence-9.0.27}/pyproject.toml +1 -1
  9. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_caller_assertions.py +32 -85
  10. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_fastapi_mcp.py +6 -4
  11. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_fastapi_request_identity.py +26 -36
  12. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_models_user_request_bound_auth.py +1 -5
  13. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_team_memberships.py +7 -18
  14. {mainsequence-9.0.26 → mainsequence-9.0.27}/LICENSE +0 -0
  15. {mainsequence-9.0.26 → mainsequence-9.0.27}/README.md +0 -0
  16. {mainsequence-9.0.26 → mainsequence-9.0.27}/agent_scaffold/skills/a2a_sdk_execution/SKILL.md +0 -0
  17. {mainsequence-9.0.26 → mainsequence-9.0.27}/agent_scaffold/skills/mainsequence-inference/SKILL.md +0 -0
  18. {mainsequence-9.0.26 → mainsequence-9.0.27}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
  19. {mainsequence-9.0.26 → mainsequence-9.0.27}/agent_scaffold/skills/maintenance/code_repository_maintenance/SKILL.md +0 -0
  20. {mainsequence-9.0.26 → mainsequence-9.0.27}/agent_scaffold/skills/maintenance/metatables_transition/SKILL.md +0 -0
  21. {mainsequence-9.0.26 → mainsequence-9.0.27}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
  22. {mainsequence-9.0.26 → mainsequence-9.0.27}/agent_scaffold/skills/sdk_code_repository_execution/SKILL.md +0 -0
  23. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/__init__.py +0 -0
  24. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/__main__.py +0 -0
  25. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/_request_identity.py +0 -0
  26. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/_team_memberships.py +0 -0
  27. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/_transport.py +0 -0
  28. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/bootstrap.py +0 -0
  29. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/cli/__init__.py +0 -0
  30. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/cli/api.py +0 -0
  31. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/cli/browser_auth.py +0 -0
  32. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/cli/cli.py +0 -0
  33. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/cli/code_repository_status.py +0 -0
  34. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/cli/config.py +0 -0
  35. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/cli/docker_utils.py +0 -0
  36. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/cli/doctor.py +0 -0
  37. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/cli/local_ops.py +0 -0
  38. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/cli/model_filters.py +0 -0
  39. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/cli/pydantic_cli.py +0 -0
  40. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/cli/sdk_utils.py +0 -0
  41. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/cli/ssh_utils.py +0 -0
  42. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/cli/ui.py +0 -0
  43. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/client/__init__.py +0 -0
  44. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/client/agent_runtime_models.py +0 -0
  45. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/client/base.py +0 -0
  46. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/client/client.py +0 -0
  47. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/client/compute_validation.py +0 -0
  48. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/client/exceptions.py +0 -0
  49. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/client/github_issues.py +0 -0
  50. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/client/inference.py +0 -0
  51. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/client/models_foundry.py +0 -0
  52. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/client/models_helpers.py +0 -0
  53. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/client/observability.py +0 -0
  54. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/client/utils.py +0 -0
  55. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/client/value_sets.py +0 -0
  56. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/code_repository_context.py +0 -0
  57. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/code_repository_skills.py +0 -0
  58. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/defaults.py +0 -0
  59. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/instrumentation/__init__.py +0 -0
  60. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/instrumentation/utils.py +0 -0
  61. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/logconf.py +0 -0
  62. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/repository_identity_security.py +0 -0
  63. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/runtime_credential_exchange.py +0 -0
  64. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/runtime_flags.py +0 -0
  65. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/scaffold_skills.py +0 -0
  66. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/server/__init__.py +0 -0
  67. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/server/_mcp_skills_upstream/__init__.py +0 -0
  68. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/server/_mcp_skills_upstream/server.py +0 -0
  69. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/server/_mcp_skills_upstream/shared.py +0 -0
  70. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence/server/mcp_skills.py +0 -0
  71. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence.egg-info/SOURCES.txt +0 -0
  72. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence.egg-info/dependency_links.txt +0 -0
  73. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence.egg-info/entry_points.txt +0 -0
  74. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence.egg-info/requires.txt +0 -0
  75. {mainsequence-9.0.26 → mainsequence-9.0.27}/mainsequence.egg-info/top_level.txt +0 -0
  76. {mainsequence-9.0.26 → mainsequence-9.0.27}/setup.cfg +0 -0
  77. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_auth_bootstrap.py +0 -0
  78. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_auth_precedence.py +0 -0
  79. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_canonical_api_route_contract.py +0 -0
  80. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_cli_auth_contract.py +0 -0
  81. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_cli_browser_auth.py +0 -0
  82. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_cli_request_budget.py +0 -0
  83. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_cli_secure_storage.py +0 -0
  84. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_client.py +0 -0
  85. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_code_repository_branch_contract.py +0 -0
  86. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_code_repository_documentation_contract.py +0 -0
  87. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_code_repository_runtime_context.py +0 -0
  88. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_code_repository_skills.py +0 -0
  89. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_credential_repr.py +0 -0
  90. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_dependency_extras.py +0 -0
  91. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_dev_release_version.py +0 -0
  92. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_environment_resource_context.py +0 -0
  93. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_environment_response_contracts.py +0 -0
  94. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_github_issues_client.py +0 -0
  95. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_http_retry_budget.py +0 -0
  96. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_independent_context.py +0 -0
  97. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_inference_client.py +0 -0
  98. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_instrumentation.py +0 -0
  99. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_job_client.py +0 -0
  100. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_local_ops.py +0 -0
  101. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_logconf.py +0 -0
  102. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_mcp_skills.py +0 -0
  103. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_notification_uid_contract.py +0 -0
  104. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_observability_client.py +0 -0
  105. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_runtime_credential_exchange.py +0 -0
  106. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_scaffold_skills.py +0 -0
  107. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_secret_client_model.py +0 -0
  108. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_source_ownership.py +0 -0
  109. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_ssh_utils.py +0 -0
  110. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_tolerant_response_reading.py +0 -0
  111. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_tolerant_value_sets.py +0 -0
  112. {mainsequence-9.0.26 → mainsequence-9.0.27}/tests/test_user_application_facts.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 9.0.26
3
+ Version: 9.0.27
4
4
  Summary: Main Sequence SDK
5
5
  Author-email: Main Sequence GmbH <dev@main-sequence.io>
6
6
  License-Expression: MIT
@@ -199,10 +199,8 @@ def publish_report(team_uid: str) -> dict[str, str]:
199
199
  Call it from a `def` handler, or from an `async def` handler with
200
200
  `await asyncio.to_thread(caller.in_teams, team_uids)`, which keeps the
201
201
  request's identity.
202
- - `team_uids` on the identity is deprecated and warns when read. In a signed
203
- HTTP request it raises `RequestIdentityError` when the assertion carried no
204
- team list, as every assertion will once the platform stops signing them.
205
- Replace every check that reads it with `in_teams()`.
202
+ - The identity has no `team_uids`; the platform signs no team list. Code
203
+ written for releases that had it reads `in_teams()` instead.
206
204
  - Membership is not team administration and not a grant on an object: check
207
205
  the exact object and action as well.
208
206
 
@@ -137,7 +137,7 @@ Keep these separate:
137
137
  - administration of the team itself
138
138
 
139
139
  An application that checks whether its caller or requester belongs to a team
140
- uses `in_teams()`, never the deprecated `team_uids`; see
140
+ uses `in_teams()`; the identity carries no team list. See
141
141
  `.agents/skills/mainsequence/application_surfaces/api_surfaces/SKILL.md`.
142
142
 
143
143
  ### 5. `Constant` vs `Secret` is a security boundary
@@ -308,9 +308,9 @@ When reviewing an access-control task, look for:
308
308
  - using direct user grants where a team should be used
309
309
  - using a team when the access is clearly one-off
310
310
  - treating team membership as team administration
311
- - team checks that read the deprecated `team_uids` instead of `in_teams()`,
312
- that read an `in_teams()` error as "not a member", or that reuse an older
313
- answer (no `max_age_seconds=0`) before a write that must see a removal
311
+ - team checks that do not use `in_teams()`, that read an `in_teams()` error
312
+ as "not a member", or that reuse an older answer (no `max_age_seconds=0`)
313
+ before a write that must see a removal
314
314
  - storing sensitive data in a `Constant`
315
315
  - creating a `Constant` or `Secret` blindly without resolving whether the name already exists
316
316
  - weak or unverified claims about who can access a resource
@@ -2,7 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  import datetime
4
4
  import math
5
- import warnings
6
5
  from collections.abc import Iterable
7
6
  from typing import Any, ClassVar, Literal
8
7
  from uuid import UUID
@@ -43,7 +42,7 @@ class RequestUserIdentity(BaseModel):
43
42
  ``in_teams()`` asks the platform which teams either of them belongs to.
44
43
  """
45
44
 
46
- model_config = ConfigDict(extra="forbid", frozen=True, populate_by_name=True)
45
+ model_config = ConfigDict(extra="forbid", frozen=True)
47
46
 
48
47
  uid: str = Field(
49
48
  ...,
@@ -58,17 +57,6 @@ class RequestUserIdentity(BaseModel):
58
57
  title="Username",
59
58
  description="Optional display name authenticated by the runtime gateway.",
60
59
  )
61
- signed_team_uids: tuple[str, ...] | None = Field(
62
- (),
63
- alias="team_uids",
64
- title="Signed team UIDs (deprecated)",
65
- description=(
66
- "Canonical UIDs of the active teams the platform signed in the caller "
67
- "assertion, None when it signed none. Deprecated: read membership with "
68
- "in_teams() (#225). Empty in local mode and on WebSockets, which carry no "
69
- "such facts."
70
- ),
71
- )
72
60
  is_organization_admin: bool = Field(
73
61
  False,
74
62
  title="Organization admin",
@@ -87,37 +75,6 @@ class RequestUserIdentity(BaseModel):
87
75
  except (AttributeError, TypeError, ValueError) as exc:
88
76
  raise ValueError("Request user UID must be a valid UUID.") from exc
89
77
 
90
- @field_validator("signed_team_uids", mode="before")
91
- @classmethod
92
- def _canonicalize_team_uids(cls, value: Any) -> tuple[str, ...] | None:
93
- if value is None:
94
- return None
95
- try:
96
- return tuple(str(UUID(str(item).strip())) for item in value)
97
- except (AttributeError, TypeError, ValueError) as exc:
98
- raise ValueError("Team UIDs must be valid UUIDs.") from exc
99
-
100
- @property
101
- def team_uids(self) -> tuple[str, ...]:
102
- """Deprecated: the team UIDs the platform signed. Use ``in_teams()``.
103
-
104
- Warns when read. When the caller assertion carried no team list it
105
- raises ``RequestIdentityError``; it never answers an empty list for
106
- that. Empty in local mode and on WebSockets, as before (#225).
107
- """
108
- warnings.warn(
109
- "RequestUserIdentity.team_uids is deprecated; check membership with "
110
- "in_teams() instead.",
111
- DeprecationWarning,
112
- stacklevel=2,
113
- )
114
- if self.signed_team_uids is None:
115
- raise RequestIdentityError(
116
- "The caller assertion carries no team list. Check team membership "
117
- "with in_teams()."
118
- )
119
- return self.signed_team_uids
120
-
121
78
  def in_teams(
122
79
  self, team_uids: Iterable[str | UUID], *, max_age_seconds: float | None = None
123
80
  ) -> frozenset[str]:
@@ -63,8 +63,6 @@ class _VerifiedRequester:
63
63
  """The person a requester-bound call works for, as the platform signed it."""
64
64
 
65
65
  user_uid: str
66
- # None when the assertion carries no team list (#225).
67
- team_uids: tuple[str, ...] | None
68
66
  is_organization_admin: bool
69
67
 
70
68
 
@@ -75,66 +73,44 @@ class AuthenticatedCaller:
75
73
  environment_uid: str
76
74
  issued_at: int
77
75
  expires_at: int
78
- # None when the assertion carries no team list (#225).
79
- team_uids: tuple[str, ...] | None
80
76
  is_organization_admin: bool
81
77
  # The person a requester-bound call works for, None on any other call. The
82
78
  # fields above stay the caller's: the acting application (ADR-0036).
83
79
  _requester: _VerifiedRequester | None = None
84
80
 
85
81
 
86
- def _team_uids(value: object, *, owner: str) -> tuple[str, ...]:
87
- if not isinstance(value, list):
88
- raise InvalidCallerAssertion(f"{owner} team UIDs are invalid.")
89
- team_uids = tuple(_canonical_uid(item) for item in value)
90
- if list(team_uids) != sorted(set(team_uids)):
91
- raise InvalidCallerAssertion(f"{owner} team UIDs must be sorted and unique.")
92
- return team_uids
93
-
94
-
95
82
  def _admin_flag(value: object, *, owner: str) -> bool:
96
83
  if type(value) is not bool:
97
84
  raise InvalidCallerAssertion(f"{owner} admin flag is invalid.")
98
85
  return value
99
86
 
100
87
 
101
- def _signed_team_uids(claims: dict, *, owner: str) -> tuple[str, ...] | None:
102
- """The deprecated ``team_uids`` while the platform still signs it, else None.
88
+ def _caller_admin_flag(payload: dict) -> bool:
89
+ """The caller's admin flag, after checking the exact claim set.
103
90
 
104
- Membership is checked on demand with ``RequestUserIdentity.in_teams()``
105
- (#225); a list that is still signed is checked as before.
91
+ The assertion names no teams: membership is checked on demand with
92
+ ``RequestUserIdentity.in_teams()`` (#225), and an assertion that lists
93
+ teams is refused.
106
94
  """
107
- if "team_uids" not in claims:
108
- return None
109
- return _team_uids(claims["team_uids"], owner=owner)
110
-
111
-
112
- def _caller_facts(payload: dict) -> tuple[tuple[str, ...] | None, bool]:
113
- """The caller's admin flag, and its team UIDs while the platform signs them."""
114
95
  # A requester-bound call also carries "requester", which _requester() checks.
115
- if set(payload) - {"requester", "team_uids"} != REQUIRED_CLAIMS:
96
+ if set(payload) - {"requester"} != REQUIRED_CLAIMS:
116
97
  raise InvalidCallerAssertion("Caller assertion claims are invalid.")
117
- return (
118
- _signed_team_uids(payload, owner="Caller"),
119
- _admin_flag(payload["is_organization_admin"], owner="Caller"),
120
- )
98
+ return _admin_flag(payload["is_organization_admin"], owner="Caller")
121
99
 
122
100
 
123
101
  def _requester(payload: dict) -> _VerifiedRequester | None:
124
102
  """The person a requester-bound call works for, or None on any other call.
125
103
 
126
104
  The claim has exactly ``sub`` and ``is_organization_admin``, the person's
127
- own User UID and Organization-admin flag, and may still carry the
128
- deprecated ``team_uids`` (#225).
105
+ own User UID and Organization-admin flag.
129
106
  """
130
107
  if "requester" not in payload:
131
108
  return None
132
109
  claim = payload["requester"]
133
- if not isinstance(claim, dict) or set(claim) - {"team_uids"} != REQUESTER_CLAIMS:
110
+ if not isinstance(claim, dict) or set(claim) != REQUESTER_CLAIMS:
134
111
  raise InvalidCallerAssertion("Caller assertion requester is invalid.")
135
112
  return _VerifiedRequester(
136
113
  user_uid=_canonical_uid(claim["sub"]),
137
- team_uids=_signed_team_uids(claim, owner="Requester"),
138
114
  is_organization_admin=_admin_flag(claim["is_organization_admin"], owner="Requester"),
139
115
  )
140
116
 
@@ -284,7 +260,7 @@ class CallerAssertionVerifier:
284
260
  audience=f"urn:mainsequence:fapi:{self.release_uid}",
285
261
  options={"require": list(REQUIRED_CLAIMS)},
286
262
  )
287
- team_uids, is_organization_admin = _caller_facts(payload)
263
+ is_organization_admin = _caller_admin_flag(payload)
288
264
  requester = _requester(payload)
289
265
  if (
290
266
  payload["aud"] != f"urn:mainsequence:fapi:{self.release_uid}"
@@ -304,7 +280,6 @@ class CallerAssertionVerifier:
304
280
  environment_uid=self.environment_uid,
305
281
  issued_at=iat,
306
282
  expires_at=exp,
307
- team_uids=team_uids,
308
283
  is_organization_admin=is_organization_admin,
309
284
  _requester=requester,
310
285
  )
@@ -229,7 +229,6 @@ class _RequestIdentityMiddleware:
229
229
  caller = await asyncio.to_thread(self.verifier.verify, assertion)
230
230
  user = RequestUserIdentity(
231
231
  uid=caller.user_uid,
232
- team_uids=caller.team_uids,
233
232
  is_organization_admin=caller.is_organization_admin,
234
233
  )
235
234
  state["resource_release_uid"] = caller.release_uid
@@ -244,7 +243,6 @@ class _RequestIdentityMiddleware:
244
243
  # The person this call works for, with the person's own facts.
245
244
  context.requester = RequestUserIdentity(
246
245
  uid=caller._requester.user_uid,
247
- team_uids=caller._requester.team_uids,
248
246
  is_organization_admin=caller._requester.is_organization_admin,
249
247
  )
250
248
  else:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 9.0.26
3
+ Version: 9.0.27
4
4
  Summary: Main Sequence SDK
5
5
  Author-email: Main Sequence GmbH <dev@main-sequence.io>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mainsequence"
7
- version = "9.0.26"
7
+ version = "9.0.27"
8
8
  description = "Main Sequence SDK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -55,7 +55,6 @@ def token(key, *, kid="first", headers=None, omit=(), **changes):
55
55
  "iat": now,
56
56
  "nbf": now,
57
57
  "exp": now + 120,
58
- "team_uids": [],
59
58
  "is_organization_admin": False,
60
59
  }
61
60
  claims.update(changes)
@@ -79,57 +78,43 @@ def verifier(fetch):
79
78
  TEAMS = sorted([str(uuid4()), str(uuid4())])
80
79
 
81
80
 
82
- def test_exposes_the_callers_teams_and_admin_flag(keys):
81
+ def test_exposes_the_callers_admin_flag_and_names_no_teams(keys):
83
82
  key, public = keys[0]
84
83
  check = verifier(lambda: {"keys": [public]})
85
84
 
86
- caller = check.verify(token(key, team_uids=TEAMS, is_organization_admin=True))
85
+ caller = check.verify(token(key, is_organization_admin=True))
87
86
 
88
- assert caller.team_uids == tuple(TEAMS)
89
87
  assert caller.is_organization_admin is True
88
+ # Membership is checked on demand with in_teams() (#225).
89
+ assert not hasattr(caller, "team_uids")
90
90
 
91
91
 
92
- def test_a_caller_in_no_team_who_is_not_an_admin(keys):
92
+ def test_a_caller_who_is_not_an_admin(keys):
93
93
  key, public = keys[0]
94
94
 
95
95
  caller = verifier(lambda: {"keys": [public]}).verify(token(key))
96
96
 
97
- assert caller.team_uids == ()
98
97
  assert caller.is_organization_admin is False
99
98
 
100
99
 
101
- @pytest.mark.parametrize(
102
- "omit",
103
- [("team_uids", "is_organization_admin"), ("is_organization_admin",)],
104
- ids=["no_facts", "no_admin_flag"],
105
- )
106
- def test_rejects_an_assertion_without_the_callers_admin_flag(keys, omit):
100
+ def test_rejects_an_assertion_without_the_callers_admin_flag(keys):
107
101
  """Every assertion states the caller's admin flag; there is no older shape."""
108
102
  key, public = keys[0]
109
103
  with pytest.raises(InvalidCallerAssertion):
110
- verifier(lambda: {"keys": [public]}).verify(token(key, omit=omit))
104
+ verifier(lambda: {"keys": [public]}).verify(token(key, omit=("is_organization_admin",)))
111
105
 
112
106
 
113
- def test_accepts_an_assertion_without_a_team_list(keys):
114
- """The platform stops signing team lists; membership is checked with in_teams (#225)."""
107
+ @pytest.mark.parametrize("team_uids", [TEAMS, [], None], ids=["team_list", "empty_list", "null"])
108
+ def test_rejects_an_assertion_that_lists_teams(keys, team_uids):
109
+ """The platform signs no team list; an assertion that carries one is refused (#225)."""
115
110
  key, public = keys[0]
116
-
117
- caller = verifier(lambda: {"keys": [public]}).verify(
118
- token(key, omit=("team_uids",), is_organization_admin=True)
119
- )
120
-
121
- assert caller.team_uids is None
122
- assert caller.is_organization_admin is True
111
+ with pytest.raises(InvalidCallerAssertion):
112
+ verifier(lambda: {"keys": [public]}).verify(token(key, team_uids=team_uids))
123
113
 
124
114
 
125
115
  @pytest.mark.parametrize(
126
116
  "changes",
127
117
  [
128
- {"team_uids": None},
129
- {"team_uids": TEAMS[0]},
130
- {"team_uids": list(reversed(TEAMS))},
131
- {"team_uids": [TEAMS[0], TEAMS[0]]},
132
- {"team_uids": [TEAMS[0].upper()]},
133
118
  {"is_organization_admin": None},
134
119
  {"is_organization_admin": "true"},
135
120
  {"is_organization_admin": 1},
@@ -146,11 +131,11 @@ def test_rejects_malformed_caller_facts(keys, changes):
146
131
  #
147
132
  # Another application calls while it works for a person, the requester. The
148
133
  # assertion's `sub` and facts stay the caller's, the acting application, and
149
- # `requester` names the person with the person's own facts: exactly `sub`,
150
- # `team_uids` and `is_organization_admin`.
134
+ # `requester` names the person with the person's own facts: exactly `sub` and
135
+ # `is_organization_admin`.
151
136
 
152
137
  PERSON = "5b0f9a8e-3c2d-4e1f-9a7b-6c5d4e3f2a1b"
153
- REQUESTER = {"sub": PERSON, "team_uids": TEAMS, "is_organization_admin": True}
138
+ REQUESTER = {"sub": PERSON, "is_organization_admin": True}
154
139
 
155
140
 
156
141
  @pytest.mark.parametrize(
@@ -160,45 +145,32 @@ REQUESTER = {"sub": PERSON, "team_uids": TEAMS, "is_organization_admin": True}
160
145
  )
161
146
  def test_exposes_the_requester_beside_the_acting_caller(keys, caller_admin, person_admin):
162
147
  key, public = keys[0]
163
- caller_teams = [str(uuid4())]
164
148
  check = verifier(lambda: {"keys": [public]})
165
149
 
166
150
  caller = check.verify(
167
151
  token(
168
152
  key,
169
- team_uids=caller_teams,
170
153
  is_organization_admin=caller_admin,
171
154
  requester={**REQUESTER, "is_organization_admin": person_admin},
172
155
  )
173
156
  )
174
157
 
175
158
  # Each identity keeps its own facts: neither admin flag reaches the other.
176
- assert (caller.user_uid, caller.team_uids, caller.is_organization_admin) == (
177
- USER,
178
- tuple(caller_teams),
179
- caller_admin,
180
- )
159
+ assert (caller.user_uid, caller.is_organization_admin) == (USER, caller_admin)
181
160
  requester = caller._requester
182
- assert (requester.user_uid, requester.team_uids, requester.is_organization_admin) == (
183
- PERSON,
184
- tuple(TEAMS),
185
- person_admin,
186
- )
161
+ assert (requester.user_uid, requester.is_organization_admin) == (PERSON, person_admin)
162
+ assert not hasattr(requester, "team_uids")
187
163
 
188
164
 
189
- def test_a_requester_in_no_team_who_is_not_an_admin(keys):
165
+ def test_a_requester_who_is_not_an_admin(keys):
190
166
  key, public = keys[0]
191
167
 
192
168
  caller = verifier(lambda: {"keys": [public]}).verify(
193
- token(key, requester={"sub": PERSON, "team_uids": [], "is_organization_admin": False})
169
+ token(key, requester={"sub": PERSON, "is_organization_admin": False})
194
170
  )
195
171
 
196
172
  requester = caller._requester
197
- assert (requester.user_uid, requester.team_uids, requester.is_organization_admin) == (
198
- PERSON,
199
- (),
200
- False,
201
- )
173
+ assert (requester.user_uid, requester.is_organization_admin) == (PERSON, False)
202
174
 
203
175
 
204
176
  def test_a_call_that_is_not_requester_bound_has_no_requester(keys):
@@ -206,7 +178,7 @@ def test_a_call_that_is_not_requester_bound_has_no_requester(keys):
206
178
  check = verifier(lambda: {"keys": [public]})
207
179
 
208
180
  assert check.verify(token(key))._requester is None
209
- assert check.verify(token(key, team_uids=TEAMS, is_organization_admin=True))._requester is None
181
+ assert check.verify(token(key, is_organization_admin=True))._requester is None
210
182
 
211
183
 
212
184
  @pytest.mark.parametrize(
@@ -214,12 +186,14 @@ def test_a_call_that_is_not_requester_bound_has_no_requester(keys):
214
186
  [
215
187
  None,
216
188
  PERSON,
217
- [PERSON, TEAMS, True],
189
+ [PERSON, True],
218
190
  {},
219
- {"sub": PERSON, "team_uids": TEAMS},
220
- {"team_uids": TEAMS, "is_organization_admin": True},
191
+ {"sub": PERSON},
192
+ {"is_organization_admin": True},
221
193
  {**REQUESTER, "username": "person"},
222
194
  {**REQUESTER, "requester": {"sub": PERSON}},
195
+ {**REQUESTER, "team_uids": TEAMS},
196
+ {**REQUESTER, "team_uids": []},
223
197
  {**REQUESTER, "is_organization_admin": None},
224
198
  {**REQUESTER, "is_organization_admin": 1},
225
199
  {**REQUESTER, "is_organization_admin": 0},
@@ -230,22 +204,18 @@ def test_a_call_that_is_not_requester_bound_has_no_requester(keys):
230
204
  {**REQUESTER, "sub": "not-a-uuid"},
231
205
  {**REQUESTER, "sub": None},
232
206
  {**REQUESTER, "sub": 1},
233
- {**REQUESTER, "team_uids": None},
234
- {**REQUESTER, "team_uids": TEAMS[0]},
235
- {**REQUESTER, "team_uids": [1]},
236
- {**REQUESTER, "team_uids": [TEAMS[0].upper()]},
237
- {**REQUESTER, "team_uids": list(reversed(TEAMS))},
238
- {**REQUESTER, "team_uids": [TEAMS[0], TEAMS[0]]},
239
207
  ],
240
208
  ids=[
241
209
  "null",
242
210
  "string",
243
211
  "list",
244
212
  "empty",
245
- "two_field_without_admin_flag",
213
+ "missing_admin_flag",
246
214
  "missing_sub",
247
215
  "extra_key",
248
216
  "nested_requester",
217
+ "team_list",
218
+ "empty_team_list",
249
219
  "null_admin_flag",
250
220
  "integer_true_admin_flag",
251
221
  "integer_false_admin_flag",
@@ -256,12 +226,6 @@ def test_a_call_that_is_not_requester_bound_has_no_requester(keys):
256
226
  "invalid_sub",
257
227
  "null_sub",
258
228
  "integer_sub",
259
- "null_team_uids",
260
- "string_team_uids",
261
- "integer_team_uid",
262
- "uppercase_team_uid",
263
- "unsorted_team_uids",
264
- "duplicate_team_uids",
265
229
  ],
266
230
  )
267
231
  def test_rejects_a_malformed_requester(keys, requester):
@@ -270,35 +234,18 @@ def test_rejects_a_malformed_requester(keys, requester):
270
234
  verifier(lambda: {"keys": [public]}).verify(token(key, requester=requester))
271
235
 
272
236
 
273
- def test_a_requester_and_caller_without_team_lists_are_accepted(keys):
274
- """Neither the caller's facts nor the requester's need a team list (#225)."""
275
- key, public = keys[0]
276
-
277
- caller = verifier(lambda: {"keys": [public]}).verify(
278
- token(
279
- key,
280
- omit=("team_uids",),
281
- requester={"sub": PERSON, "is_organization_admin": True},
282
- )
283
- )
284
-
285
- assert caller.team_uids is None
286
- assert (caller._requester.user_uid, caller._requester.team_uids) == (PERSON, None)
287
- assert caller._requester.is_organization_admin is True
288
-
289
-
290
237
  @pytest.mark.parametrize(
291
238
  ("changes", "omit"),
292
239
  [
293
240
  ({"extra": "not an identity claim"}, ()),
294
241
  ({"requester_is_organization_admin": True}, ()),
295
- ({}, ("team_uids", "is_organization_admin")),
242
+ ({"team_uids": TEAMS}, ()),
296
243
  ({}, ("is_organization_admin",)),
297
244
  ],
298
245
  ids=[
299
246
  "unknown_claim",
300
247
  "requester_admin_claim",
301
- "no_caller_facts",
248
+ "caller_team_list",
302
249
  "no_caller_admin_flag",
303
250
  ],
304
251
  )
@@ -254,14 +254,16 @@ def test_a_tool_with_an_optional_person_never_combines_grants(signed):
254
254
 
255
255
  REJECTED_ASSERTIONS = {
256
256
  "missing": None,
257
- "two_field_requester": dict(requester={"sub": PERSON, "team_uids": []}),
257
+ "requester_without_admin_flag": dict(requester={"sub": PERSON}),
258
+ "requester_with_a_team_list": dict(requester={**requester_claim(PERSON), "team_uids": []}),
258
259
  "requester_extra_key": dict(requester={**requester_claim(PERSON), "username": "person"}),
259
260
  "requester_integer_admin_flag": dict(
260
261
  requester=requester_claim(PERSON, is_organization_admin=1)
261
262
  ),
262
- "without_caller_facts": dict(omit=("team_uids", "is_organization_admin")),
263
- "requester_bound_without_caller_facts": dict(
264
- requester=requester_claim(PERSON), omit=("team_uids", "is_organization_admin")
263
+ "without_caller_admin_flag": dict(omit=("is_organization_admin",)),
264
+ "with_a_caller_team_list": dict(team_uids=[]),
265
+ "requester_bound_without_caller_admin_flag": dict(
266
+ requester=requester_claim(PERSON), omit=("is_organization_admin",)
265
267
  ),
266
268
  }
267
269
 
@@ -69,7 +69,6 @@ def signed(monkeypatch):
69
69
  iat=now,
70
70
  nbf=now,
71
71
  exp=now + 120,
72
- team_uids=[],
73
72
  is_organization_admin=False,
74
73
  )
75
74
  claims.update(changes)
@@ -114,21 +113,18 @@ def test_sync_getter_signed_proof_once_and_process_credentials_untouched(signed,
114
113
 
115
114
  def test_signed_caller_facts_reach_the_logged_user(signed):
116
115
  token, _, _ = signed
117
- teams = sorted([str(uuid4()), str(uuid4())])
118
116
 
119
117
  def handler():
120
118
  user = User.get_logged_user()
121
- return {"teams": list(user.team_uids), "admin": user.is_organization_admin}
119
+ return {"admin": user.is_organization_admin, "teams": hasattr(user, "team_uids")}
122
120
 
123
121
  with TestClient(app_for(handler)) as client:
124
- admin = client.get(
125
- "/me",
126
- headers={ASSERTION_HEADER: token(team_uids=teams, is_organization_admin=True)},
127
- )
128
- member_of_none = client.get("/me", headers={ASSERTION_HEADER: token()})
122
+ admin = client.get("/me", headers={ASSERTION_HEADER: token(is_organization_admin=True)})
123
+ member = client.get("/me", headers={ASSERTION_HEADER: token()})
129
124
 
130
- assert admin.json() == {"teams": teams, "admin": True}
131
- assert member_of_none.json() == {"teams": [], "admin": False}
125
+ # The identity names no teams: membership is checked with in_teams() (#225).
126
+ assert admin.json() == {"admin": True, "teams": False}
127
+ assert member.json() == {"admin": False, "teams": False}
132
128
 
133
129
 
134
130
  @pytest.mark.parametrize(
@@ -139,7 +135,7 @@ def test_signed_caller_facts_reach_the_logged_user(signed):
139
135
  "expired",
140
136
  "wrong_target",
141
137
  "duplicate",
142
- "without_caller_facts",
138
+ "with_a_team_list",
143
139
  "without_admin_flag",
144
140
  ],
145
141
  )
@@ -164,8 +160,8 @@ def test_invalid_proof_never_reaches_route(signed, case, monkeypatch):
164
160
  headers[ASSERTION_HEADER] = token(resource_release_uid=OTHER)
165
161
  elif case == "duplicate":
166
162
  headers = [(ASSERTION_HEADER, token()), (ASSERTION_HEADER, token())]
167
- elif case == "without_caller_facts":
168
- headers[ASSERTION_HEADER] = token(omit=("team_uids", "is_organization_admin"))
163
+ elif case == "with_a_team_list":
164
+ headers[ASSERTION_HEADER] = token(team_uids=[])
169
165
  elif case == "without_admin_flag":
170
166
  headers[ASSERTION_HEADER] = token(omit=("is_organization_admin",))
171
167
  assert TestClient(app_for(handler)).get("/me", headers=headers).status_code == 401
@@ -575,24 +571,18 @@ def test_reads_as_caller_needs_a_signed_http_request(monkeypatch):
575
571
  PERSON = "5b0f9a8e-3c2d-4e1f-9a7b-6c5d4e3f2a1b"
576
572
 
577
573
 
578
- def requester_claim(sub=PERSON, team_uids=(), is_organization_admin=False):
574
+ def requester_claim(sub=PERSON, is_organization_admin=False):
579
575
  """The requester claim as the platform signs it."""
580
- return {
581
- "sub": sub,
582
- "team_uids": list(team_uids),
583
- "is_organization_admin": is_organization_admin,
584
- }
576
+ return {"sub": sub, "is_organization_admin": is_organization_admin}
585
577
 
586
578
 
587
579
  def _identities():
588
580
  caller = User.get_logged_user()
589
581
  requester = User.get_requester()
590
582
  return {
591
- "caller": [caller.uid, list(caller.team_uids), caller.is_organization_admin],
583
+ "caller": [caller.uid, caller.is_organization_admin],
592
584
  "requester": (
593
- None
594
- if requester is None
595
- else [requester.uid, list(requester.team_uids), requester.is_organization_admin]
585
+ None if requester is None else [requester.uid, requester.is_organization_admin]
596
586
  ),
597
587
  }
598
588
 
@@ -606,11 +596,9 @@ def test_requester_bound_call_binds_the_requester_beside_the_caller(
606
596
  signed, caller_admin, person_admin
607
597
  ):
608
598
  token, _, _ = signed
609
- caller_teams = sorted([str(uuid4()), str(uuid4())])
610
- requester_teams = sorted([str(uuid4()), str(uuid4())])
611
599
  # Each identity keeps its own facts: neither admin flag reaches the other.
612
- facts = dict(team_uids=caller_teams, is_organization_admin=caller_admin)
613
- person = requester_claim(team_uids=requester_teams, is_organization_admin=person_admin)
600
+ facts = dict(is_organization_admin=caller_admin)
601
+ person = requester_claim(is_organization_admin=person_admin)
614
602
 
615
603
  with TestClient(app_for(_identities)) as client:
616
604
  bound = client.get("/me", headers={ASSERTION_HEADER: token(**facts, requester=person)})
@@ -622,10 +610,10 @@ def test_requester_bound_call_binds_the_requester_beside_the_caller(
622
610
  )
623
611
 
624
612
  assert bound.json() == {
625
- "caller": [USER, caller_teams, caller_admin],
626
- "requester": [PERSON, requester_teams, person_admin],
613
+ "caller": [USER, caller_admin],
614
+ "requester": [PERSON, person_admin],
627
615
  }
628
- assert direct.json() == {"caller": [USER, caller_teams, caller_admin], "requester": None}
616
+ assert direct.json() == {"caller": [USER, caller_admin], "requester": None}
629
617
  with pytest.raises(RequestIdentityError, match=r"User\.get_requester\(\)"):
630
618
  User.get_requester()
631
619
 
@@ -634,23 +622,25 @@ def test_requester_bound_call_binds_the_requester_beside_the_caller(
634
622
  ("requester", "omit"),
635
623
  [
636
624
  (None, ()),
637
- ({"sub": PERSON, "team_uids": []}, ()),
625
+ ({"sub": PERSON}, ()),
626
+ ({**requester_claim(), "team_uids": []}, ()),
638
627
  ({**requester_claim(), "username": "person"}, ()),
639
628
  (requester_claim(is_organization_admin=1), ()),
640
629
  (requester_claim(is_organization_admin="true"), ()),
641
630
  (requester_claim(is_organization_admin=None), ()),
642
631
  (requester_claim(sub=PERSON.upper()), ()),
643
- (requester_claim(), ("team_uids", "is_organization_admin")),
632
+ (requester_claim(), ("is_organization_admin",)),
644
633
  ],
645
634
  ids=[
646
635
  "null",
647
- "two_field_without_admin_flag",
636
+ "without_admin_flag",
637
+ "with_a_team_list",
648
638
  "extra_key",
649
639
  "integer_admin_flag",
650
640
  "string_admin_flag",
651
641
  "null_admin_flag",
652
642
  "non_canonical_sub",
653
- "without_caller_facts",
643
+ "without_caller_admin_flag",
654
644
  ],
655
645
  )
656
646
  def test_malformed_requester_never_reaches_route(signed, requester, omit):
@@ -682,7 +672,7 @@ def test_requester_is_none_in_local_mode_and_on_websockets(monkeypatch):
682
672
  install_request_identity(app)
683
673
  client = TestClient(app)
684
674
 
685
- expected = {"caller": [USER, [], False], "requester": None}
675
+ expected = {"caller": [USER, False], "requester": None}
686
676
  assert client.get("/me", headers={"Authorization": "Bearer local-token"}).json() == expected
687
677
  with client.websocket_connect("/ws", headers={"X-User-UID": USER}) as connection:
688
678
  assert connection.receive_json() == expected
@@ -766,5 +756,5 @@ def test_reads_as_caller_refuses_a_requester_bound_request(signed, monkeypatch):
766
756
  with TestClient(app_for(handler)) as client:
767
757
  response = client.get("/me", headers={ASSERTION_HEADER: token(requester=person)})
768
758
 
769
- assert response.json() == {"caller": [USER, [], False], "requester": [PERSON, [], True]}
759
+ assert response.json() == {"caller": [USER, False], "requester": [PERSON, True]}
770
760
  assert sent == []
@@ -51,7 +51,6 @@ def test_process_profile_still_uses_process_session(monkeypatch):
51
51
  # A requester-bound request: the caller is the acting application, and the
52
52
  # request scope also holds the person it works for (ADR-0036).
53
53
  REQUESTER_UID = "5b0f9a8e-3c2d-4e1f-9a7b-6c5d4e3f2a1b"
54
- TEAM_UID = "3f1cc452-43ec-49cb-b2ba-87dbac164d29"
55
54
 
56
55
 
57
56
  def test_requester_getter_requires_authenticated_request():
@@ -67,15 +66,12 @@ def test_requester_getter_requires_authenticated_request():
67
66
 
68
67
  def test_requester_getter_returns_the_requester_beside_the_unchanged_caller():
69
68
  caller = RequestUserIdentity(uid=USER_UID)
70
- requester = RequestUserIdentity(
71
- uid=REQUESTER_UID, team_uids=[TEAM_UID], is_organization_admin=True
72
- )
69
+ requester = RequestUserIdentity(uid=REQUESTER_UID, is_organization_admin=True)
73
70
  with _request_scope() as context:
74
71
  context.user = caller
75
72
  assert User.get_requester() is None
76
73
  context.requester = requester
77
74
  assert User.get_requester() is requester
78
- assert User.get_requester().team_uids == (TEAM_UID,)
79
75
  # The person's own admin flag, never the caller's.
80
76
  assert User.get_requester().is_organization_admin is True
81
77
  assert User.get_logged_user().is_organization_admin is False
@@ -7,7 +7,6 @@ memberships and records every call.
7
7
 
8
8
  import asyncio
9
9
  import time
10
- import warnings
11
10
  from types import SimpleNamespace
12
11
  from unittest.mock import Mock
13
12
  from uuid import uuid4
@@ -393,11 +392,7 @@ def test_local_mode_has_no_assertion_to_ask_with(platform, monkeypatch):
393
392
  )
394
393
 
395
394
  def handler():
396
- user = User.get_logged_user()
397
- with warnings.catch_warnings():
398
- warnings.simplefilter("ignore", DeprecationWarning)
399
- assert user.team_uids == ()
400
- user.in_teams(teams(1))
395
+ User.get_logged_user().in_teams(teams(1))
401
396
 
402
397
  with TestClient(app_with(handler), raise_server_exceptions=True) as client:
403
398
  with pytest.raises(RequestIdentityError):
@@ -405,26 +400,20 @@ def test_local_mode_has_no_assertion_to_ask_with(platform, monkeypatch):
405
400
  assert platform.calls == []
406
401
 
407
402
 
408
- def test_team_uids_is_deprecated_and_never_answers_empty_for_a_missing_list(sign, platform):
409
- (research,) = teams(1)
403
+ def test_the_identity_has_no_team_list(sign, platform):
404
+ # Step 5 (#225): the deprecated team_uids is gone; in_teams() answers.
410
405
  seen = {}
411
406
 
412
407
  def handler():
413
408
  user = User.get_logged_user()
414
- with pytest.warns(DeprecationWarning):
415
- try:
416
- seen["teams"] = user.team_uids
417
- except RequestIdentityError as error:
418
- seen["error"] = str(error)
409
+ seen["has_team_uids"] = hasattr(user, "team_uids")
419
410
 
420
411
  with TestClient(app_with(handler)) as client:
421
- client.get("/check", headers={ASSERTION_HEADER: sign(team_uids=[research])})
422
- signed = dict(seen)
423
- seen.clear()
424
412
  client.get("/check", headers={ASSERTION_HEADER: sign()})
425
413
 
426
- assert signed == {"teams": (research,)}
427
- assert "in_teams()" in seen["error"]
414
+ assert seen == {"has_team_uids": False}
415
+ with pytest.raises(ValueError):
416
+ RequestUserIdentity(uid=CALLER, team_uids=[])
428
417
 
429
418
 
430
419
  @pytest.mark.parametrize(
File without changes
File without changes
File without changes