mainsequence 9.0.26.dev70__py3-none-any.whl → 9.0.27.dev71__py3-none-any.whl
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.
- agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +2 -4
- agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +4 -4
- mainsequence/client/models_user.py +1 -44
- mainsequence/server/caller_assertions.py +10 -35
- mainsequence/server/fastapi.py +0 -2
- {mainsequence-9.0.26.dev70.dist-info → mainsequence-9.0.27.dev71.dist-info}/METADATA +1 -1
- {mainsequence-9.0.26.dev70.dist-info → mainsequence-9.0.27.dev71.dist-info}/RECORD +11 -11
- {mainsequence-9.0.26.dev70.dist-info → mainsequence-9.0.27.dev71.dist-info}/WHEEL +0 -0
- {mainsequence-9.0.26.dev70.dist-info → mainsequence-9.0.27.dev71.dist-info}/entry_points.txt +0 -0
- {mainsequence-9.0.26.dev70.dist-info → mainsequence-9.0.27.dev71.dist-info}/licenses/LICENSE +0 -0
- {mainsequence-9.0.26.dev70.dist-info → mainsequence-9.0.27.dev71.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
203
|
-
|
|
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()
|
|
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
|
|
312
|
-
|
|
313
|
-
|
|
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
|
|
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
|
|
102
|
-
"""The
|
|
88
|
+
def _caller_admin_flag(payload: dict) -> bool:
|
|
89
|
+
"""The caller's admin flag, after checking the exact claim set.
|
|
103
90
|
|
|
104
|
-
|
|
105
|
-
(#225)
|
|
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"
|
|
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
|
|
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)
|
|
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
|
-
|
|
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
|
)
|
mainsequence/server/fastapi.py
CHANGED
|
@@ -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,10 +1,10 @@
|
|
|
1
1
|
agent_scaffold/skills/a2a_sdk_execution/SKILL.md,sha256=NcwIQ1QgD4Ku0mG2Hb-Izee8p9fm9LTPHa6gDEWTOSQ,13200
|
|
2
|
-
agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md,sha256=
|
|
2
|
+
agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md,sha256=V2YW1oNlkKCr0_WwieVIZStl7rRfXlk02IdvkzyuneU,26820
|
|
3
3
|
agent_scaffold/skills/mainsequence-inference/SKILL.md,sha256=yv8aAHLQ2FBBSWPIFadY7pGNTwtR-yiR_-UzaGRN9-g,12502
|
|
4
4
|
agent_scaffold/skills/maintenance/bug_auditor/SKILL.md,sha256=8RRZNmC3omhZoxNbmu50MdhBFTQ2CU2THeyNu07Ax18,8659
|
|
5
5
|
agent_scaffold/skills/maintenance/code_repository_maintenance/SKILL.md,sha256=pNQICoVW6nRr0HfvyB0Q64TdsqCXTWQUuImpBL-4XUQ,18575
|
|
6
6
|
agent_scaffold/skills/maintenance/metatables_transition/SKILL.md,sha256=PisgpytcksJnfZMS6aiPCmahsT3fm8DIwk_cefXA2qU,3759
|
|
7
|
-
agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md,sha256=
|
|
7
|
+
agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md,sha256=sgAe4buJaMoatRRwBVWZvDcONY9mxJyqElUFX5swoqA,15249
|
|
8
8
|
agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md,sha256=KfTptbOMO-b8hiVFPgKxRMjmQWZCLB9Sr_kli7kp4hs,16920
|
|
9
9
|
agent_scaffold/skills/sdk_code_repository_execution/SKILL.md,sha256=g4C0Cp5oqQg2LBjT-piWXunmt_hNd5yTK33RMYPAE8I,7313
|
|
10
10
|
mainsequence/__init__.py,sha256=70CbIo7ZG1mzi5uwkqDtVESunBE0RanHjeFg4ptfsA0,385
|
|
@@ -45,22 +45,22 @@ mainsequence/client/github_issues.py,sha256=pSTUw3UPrmE6wqnpoJctartTaNSq6iu6PQRr
|
|
|
45
45
|
mainsequence/client/inference.py,sha256=tYKHRxLR6mA5iLmgd3qURwweWc2OPQuw0__tD3LQgSI,7748
|
|
46
46
|
mainsequence/client/models_foundry.py,sha256=JwQdPLDSO4dZI-KC0SUdmN6izmNIKMb9IzZlYXy0Mig,36488
|
|
47
47
|
mainsequence/client/models_helpers.py,sha256=CQRjBjx3DsREJLRk0vqFsC-Dcll5lUP2ofu6e5UMvhk,49051
|
|
48
|
-
mainsequence/client/models_user.py,sha256=
|
|
48
|
+
mainsequence/client/models_user.py,sha256=Z3h6MX5aWKFfEvtwHPk1adhfPbG3RZCKbp3Aanor4lo,50502
|
|
49
49
|
mainsequence/client/observability.py,sha256=_WrDKCaNk_TXmKElKCXC1inork6Exxxe8FRY4m1GI9s,16008
|
|
50
50
|
mainsequence/client/utils.py,sha256=Rk2z-78swO6GepibLlQNdxL1eqYy8vHGrjiZdV3G5CM,31641
|
|
51
51
|
mainsequence/client/value_sets.py,sha256=7TA-mpuAd3hOduNgO1WTQQTuNOGaFgue-VqCsDWKaC8,6181
|
|
52
52
|
mainsequence/instrumentation/__init__.py,sha256=J_9XmfjprElvz0u2t9rTyJT0kFdyKpxdbkdQiaw28_U,316
|
|
53
53
|
mainsequence/instrumentation/utils.py,sha256=b-ihjSioAepeLfMpudM2SqCtvnxMqIXileHx3klBCQU,3377
|
|
54
54
|
mainsequence/server/__init__.py,sha256=BHUAhtdWPDRdxX07NYO_6wrBv5mf0GsXflecO52PmUc,73
|
|
55
|
-
mainsequence/server/caller_assertions.py,sha256=
|
|
56
|
-
mainsequence/server/fastapi.py,sha256=
|
|
55
|
+
mainsequence/server/caller_assertions.py,sha256=IUQ74UL3VqCAAuujTGk3DzCPogRYhWRD9rVoWezdWdU,11979
|
|
56
|
+
mainsequence/server/fastapi.py,sha256=M5zDoFIu9MzuQZdLOXvQPU9Yyqyf_GyXWVn_aravHH4,20123
|
|
57
57
|
mainsequence/server/mcp_skills.py,sha256=-krRwAHCAMipScgGJ19uVkzznhTop1Nz4C_wRY1GUpw,16690
|
|
58
58
|
mainsequence/server/_mcp_skills_upstream/__init__.py,sha256=px283qBMf3z1nxCfN3WHrH9v8Y8jOb1xoBqZMRZcaD8,2694
|
|
59
59
|
mainsequence/server/_mcp_skills_upstream/server.py,sha256=laHsfpoaYX3LcwQ04-GN7r0_C63fG6CzyXDci2rmbHE,7695
|
|
60
60
|
mainsequence/server/_mcp_skills_upstream/shared.py,sha256=JmnX9PoLoyi93nBYnUEPWCdPXor_nQHjHptQDXpFzPs,11978
|
|
61
|
-
mainsequence-9.0.
|
|
62
|
-
mainsequence-9.0.
|
|
63
|
-
mainsequence-9.0.
|
|
64
|
-
mainsequence-9.0.
|
|
65
|
-
mainsequence-9.0.
|
|
66
|
-
mainsequence-9.0.
|
|
61
|
+
mainsequence-9.0.27.dev71.dist-info/licenses/LICENSE,sha256=b6Ecsk8wjsEby3lbSzdljR6hHiFGHBizt9iWV2p1c_I,1074
|
|
62
|
+
mainsequence-9.0.27.dev71.dist-info/METADATA,sha256=fsXFdkTMGMsdDs7W1h_085WrAvjdsZrLr35ldgLPRpw,7531
|
|
63
|
+
mainsequence-9.0.27.dev71.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
64
|
+
mainsequence-9.0.27.dev71.dist-info/entry_points.txt,sha256=XGAOq3-mF2lWWpeRJvoaWyMarRcK167mH1g124aGdUs,88
|
|
65
|
+
mainsequence-9.0.27.dev71.dist-info/top_level.txt,sha256=BFeblM_Q9q0cUWBvTxnfWcWQEHtf5XZi9nOS-Gdfwus,28
|
|
66
|
+
mainsequence-9.0.27.dev71.dist-info/RECORD,,
|
|
File without changes
|
{mainsequence-9.0.26.dev70.dist-info → mainsequence-9.0.27.dev71.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{mainsequence-9.0.26.dev70.dist-info → mainsequence-9.0.27.dev71.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|