mainsequence 9.0.14.dev56__py3-none-any.whl → 9.0.15.dev57__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/AGENTS.md +2 -1
- agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +51 -0
- agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +86 -1
- mainsequence/_request_identity.py +22 -3
- mainsequence/client/models_user.py +36 -4
- mainsequence/server/caller_assertions.py +43 -7
- mainsequence/server/fastapi.py +14 -2
- {mainsequence-9.0.14.dev56.dist-info → mainsequence-9.0.15.dev57.dist-info}/METADATA +1 -1
- {mainsequence-9.0.14.dev56.dist-info → mainsequence-9.0.15.dev57.dist-info}/RECORD +13 -13
- {mainsequence-9.0.14.dev56.dist-info → mainsequence-9.0.15.dev57.dist-info}/WHEEL +0 -0
- {mainsequence-9.0.14.dev56.dist-info → mainsequence-9.0.15.dev57.dist-info}/entry_points.txt +0 -0
- {mainsequence-9.0.14.dev56.dist-info → mainsequence-9.0.15.dev57.dist-info}/licenses/LICENSE +0 -0
- {mainsequence-9.0.14.dev56.dist-info → mainsequence-9.0.15.dev57.dist-info}/top_level.txt +0 -0
agent_scaffold/AGENTS.md
CHANGED
|
@@ -81,7 +81,8 @@ bundle. Platform skills are installed separately under
|
|
|
81
81
|
`.agents/skills/mainsequence/application_surfaces/api_surfaces/SKILL.md`
|
|
82
82
|
- Jobs, schedules, images, resources, releases, and Artifacts:
|
|
83
83
|
`.agents/skills/mainsequence/platform_operations/orchestration_and_releases/SKILL.md`
|
|
84
|
-
- RBAC, sharing, constants, secrets, and
|
|
84
|
+
- RBAC, sharing, constants, secrets, access verification, and whether an
|
|
85
|
+
application uses its own grants or acts for its requester:
|
|
85
86
|
`.agents/skills/mainsequence/platform_operations/access_control_and_sharing/SKILL.md`
|
|
86
87
|
- Direct recorded model calls, structured extraction, thinking/provider options,
|
|
87
88
|
inference history and idempotent replay:
|
|
@@ -160,6 +160,57 @@ connection is active. See the version-matched
|
|
|
160
160
|
[FastAPI identity documentation](https://mainsequence-sdk.github.io/mainsequence-sdk/knowledge/fastapi/)
|
|
161
161
|
for the lifecycle and trust contract.
|
|
162
162
|
|
|
163
|
+
### Requester-bound calls
|
|
164
|
+
|
|
165
|
+
Another application, for example an Agent, can call the API while it works for
|
|
166
|
+
a person: the requester. The platform signs the requester into the caller
|
|
167
|
+
assertion it sends to the API. `User.get_logged_user()` still returns the
|
|
168
|
+
caller, the acting application. `User.get_requester()` returns the requester,
|
|
169
|
+
or `None` when the call is not requester-bound:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
from fastapi import HTTPException
|
|
173
|
+
from mainsequence.client import RequestUserIdentity, User
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def require_requester() -> RequestUserIdentity:
|
|
177
|
+
"""The person this call works for; refuse the call when there is none."""
|
|
178
|
+
requester = User.get_requester()
|
|
179
|
+
if requester is None:
|
|
180
|
+
raise HTTPException(status_code=403, detail="This operation needs a requester.")
|
|
181
|
+
return requester
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@app.get("/team-reports/{team_uid}")
|
|
185
|
+
def team_report(team_uid: str) -> dict[str, str]:
|
|
186
|
+
requester = require_requester()
|
|
187
|
+
if team_uid not in requester.team_uids:
|
|
188
|
+
raise HTTPException(status_code=403, detail="The requester is not in this team.")
|
|
189
|
+
return {"team_uid": team_uid, "requested_by": requester.uid}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
- Authorize a requester-bound call against the requester. The acting
|
|
193
|
+
application has no rights of its own unless the API's policy grants them.
|
|
194
|
+
- Fail closed: an operation that needs a person refuses the call when
|
|
195
|
+
`User.get_requester()` is `None`. Never fall back to the caller's rights.
|
|
196
|
+
- Never read a person's UID from the request body, a header or a query
|
|
197
|
+
parameter, and never let a caller name whom it acts for. Anyone can write a
|
|
198
|
+
UID there. Only the signed assertion names the requester, and the SDK has
|
|
199
|
+
verified that it is addressed to this release.
|
|
200
|
+
- The requester carries the `team_uids` the platform signed, and its
|
|
201
|
+
`is_organization_admin` is always `False`: a requester's access reaches the
|
|
202
|
+
API at member level only.
|
|
203
|
+
- Inside a requester-bound call, `reads_as_caller()` raises
|
|
204
|
+
`RequestIdentityError`: the requester's access is not passed on.
|
|
205
|
+
- Test both paths: the requester-bound call authorized against the requester,
|
|
206
|
+
and the same operation refused without a requester.
|
|
207
|
+
|
|
208
|
+
Which applications may act for their requester, what that access covers, and
|
|
209
|
+
what people are told:
|
|
210
|
+
`.agents/skills/mainsequence/platform_operations/access_control_and_sharing/SKILL.md`.
|
|
211
|
+
See also
|
|
212
|
+
[Requester-bound calls](https://mainsequence-sdk.github.io/mainsequence-sdk/knowledge/fastapi/#requester-bound-calls).
|
|
213
|
+
|
|
163
214
|
### Public provider callbacks and webhooks
|
|
164
215
|
|
|
165
216
|
An external OAuth redirect or webhook cannot supply a Main Sequence Bearer token.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: mainsequence-access-control-and-sharing
|
|
3
|
-
description: Use this skill when the task is about RBAC, resource sharing, or access verification in a Main Sequence CodeRepository. This skill owns organization and team access concepts, view and edit semantics, choosing the correct shareable resource boundary,
|
|
3
|
+
description: Use this skill when the task is about RBAC, resource sharing, or access verification in a Main Sequence CodeRepository. This skill owns organization and team access concepts, view and edit semantics, choosing the correct shareable resource boundary, access checks across CodeRepositories, constants, secrets, buckets, artifacts, and releases, and whether an application uses its own grants or acts for its requester (`acts_for_requester`). It does not own job scheduling, domain-package data behavior, or API route design.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Main Sequence Access Control And Sharing
|
|
@@ -34,6 +34,8 @@ This skill is for:
|
|
|
34
34
|
- choose whether configuration belongs in a `Constant` or a `Secret`
|
|
35
35
|
- review CLI sharing flows for existing resources
|
|
36
36
|
- verify access assumptions before claiming a workflow is shareable
|
|
37
|
+
- decide whether an application uses its own grants or acts for its
|
|
38
|
+
requester, and explain what acting for the requester allows
|
|
37
39
|
|
|
38
40
|
## This Skill Must Not Claim
|
|
39
41
|
|
|
@@ -192,6 +194,81 @@ If the task claims a resource is shareable, readable, or maintainable by another
|
|
|
192
194
|
|
|
193
195
|
Do not claim access based only on naming, role titles, or intuition.
|
|
194
196
|
|
|
197
|
+
## Application Access: Own Grants Or Acting For Its Requester
|
|
198
|
+
|
|
199
|
+
This section is the reference for how a deployed application, a FastAPI
|
|
200
|
+
release or an Agent, reaches platform data.
|
|
201
|
+
|
|
202
|
+
### Its own access: grants to its workload User
|
|
203
|
+
|
|
204
|
+
An application runs as its own workload User (`workload_user_uid` on the
|
|
205
|
+
`ResourceRelease`, `Job` or `Agent`). Its own access comes from grants to that
|
|
206
|
+
User: share what it needs with that User as with any person, with `view`
|
|
207
|
+
unless it must maintain the object. These grants cover every call the
|
|
208
|
+
application makes for itself.
|
|
209
|
+
|
|
210
|
+
### Acting for its requester: `acts_for_requester`
|
|
211
|
+
|
|
212
|
+
An application can instead be enabled to act for its requester: the person
|
|
213
|
+
whose own request it is serving. The platform setting is `acts_for_requester`.
|
|
214
|
+
|
|
215
|
+
- It is `false` by default. Nothing changes for an application that leaves it
|
|
216
|
+
off.
|
|
217
|
+
- Only Organization admins enable it, in one of two ways:
|
|
218
|
+
- after the application exists, with
|
|
219
|
+
`PATCH /api/v1/workload-users/<workload_user_uid>/`;
|
|
220
|
+
- in code, by declaring `acts_for_requester: true` on the resource, a
|
|
221
|
+
`fastapi` or `harness_agent` one, in the repository workflow file:
|
|
222
|
+
|
|
223
|
+
```yaml
|
|
224
|
+
resources:
|
|
225
|
+
- key: api
|
|
226
|
+
kind: fastapi
|
|
227
|
+
spec:
|
|
228
|
+
source_path: api/main.py
|
|
229
|
+
acts_for_requester: true
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Take the exact field from the branch's workflow template and validate the
|
|
233
|
+
file before committing it. A declaration takes effect only when the person
|
|
234
|
+
who pushed it is an Organization admin. Otherwise that resource fails
|
|
235
|
+
before it deploys.
|
|
236
|
+
- An application that should act only for people, for example a data analyst
|
|
237
|
+
Agent, holds no grants of its own. Do not share data with its workload User
|
|
238
|
+
to make it work: everyone who can use the application could then reach that
|
|
239
|
+
data through it.
|
|
240
|
+
|
|
241
|
+
### What the requester's access covers
|
|
242
|
+
|
|
243
|
+
- Read only: the application never changes, shares or deletes anything with it.
|
|
244
|
+
- The requester's member level, never an admin's, even when the requester is
|
|
245
|
+
an Organization admin.
|
|
246
|
+
- Never secret values.
|
|
247
|
+
- At most 24 hours after the person's request, and only while the work for that
|
|
248
|
+
request runs.
|
|
249
|
+
- Checked again on every call, so it stops the moment the person's access ends.
|
|
250
|
+
- Never passed on to another application.
|
|
251
|
+
|
|
252
|
+
### Never accept a person's UID from a request
|
|
253
|
+
|
|
254
|
+
The requester comes only from the platform. An application never accepts a
|
|
255
|
+
person's UID from a request body, header or query parameter, and never lets a
|
|
256
|
+
caller choose whom it acts for. A FastAPI application that receives a
|
|
257
|
+
requester-bound call reads the requester with `User.get_requester()` and
|
|
258
|
+
authorizes the call against that person:
|
|
259
|
+
`.agents/skills/mainsequence/application_surfaces/api_surfaces/SKILL.md`.
|
|
260
|
+
|
|
261
|
+
### What people are told
|
|
262
|
+
|
|
263
|
+
Every client that shows an application with `acts_for_requester` shows this
|
|
264
|
+
statement:
|
|
265
|
+
|
|
266
|
+
> **This Agent works with your identity, securely.** It reads only what you can already read, only to answer your own requests, and for at most 24 hours after you ask. It cannot act as anyone else, cannot change, share or delete anything, never sees your secret values, and stops the moment your access ends. Your Organization's administrator approved it to work this way.
|
|
267
|
+
|
|
268
|
+
Show the plain limit with it: while it works on your request, the Agent's code
|
|
269
|
+
can read what you can read, which is why only administrators decide which
|
|
270
|
+
Agents may work this way.
|
|
271
|
+
|
|
195
272
|
## Review Rules
|
|
196
273
|
|
|
197
274
|
When reviewing an access-control task, look for:
|
|
@@ -205,6 +282,10 @@ When reviewing an access-control task, look for:
|
|
|
205
282
|
- creating a `Constant` or `Secret` blindly without resolving whether the name already exists
|
|
206
283
|
- weak or unverified claims about who can access a resource
|
|
207
284
|
- confusion between access policy and deployment workflow
|
|
285
|
+
- grants to an application that should act only for people
|
|
286
|
+
- an application that takes a person's UID from a request instead of from the
|
|
287
|
+
platform
|
|
288
|
+
- `acts_for_requester` presented as something a non-admin can enable
|
|
208
289
|
|
|
209
290
|
## Validation Checklist
|
|
210
291
|
|
|
@@ -221,6 +302,8 @@ Do not claim success until you have checked:
|
|
|
221
302
|
- any `Constant` or `Secret` creation path first resolved existence by name when idempotency matters
|
|
222
303
|
- the access claim was verified against the actual resource path
|
|
223
304
|
- the task did not confuse sharing policy with orchestration or producer logic
|
|
305
|
+
- an application that acts for its requester takes the person only from the
|
|
306
|
+
platform, and its own grants are intentional
|
|
224
307
|
|
|
225
308
|
## This Skill Must Stop And Escalate When
|
|
226
309
|
|
|
@@ -230,5 +313,7 @@ Do not claim success until you have checked:
|
|
|
230
313
|
- the task asks for sensitive data to be stored in a `Constant`
|
|
231
314
|
- the task assumes cross-organization sharing without explicit documentation
|
|
232
315
|
- the request requires a policy decision the user has not made
|
|
316
|
+
- the task needs `acts_for_requester` and no Organization admin has decided to
|
|
317
|
+
enable it
|
|
233
318
|
|
|
234
319
|
Do not guess through security boundaries.
|
|
@@ -20,6 +20,10 @@ class _RequestScope:
|
|
|
20
20
|
# The verified assertion of a hosted HTTP request, kept only so that
|
|
21
21
|
# reads_as_caller() can present it (ADR-0036, amended for #198).
|
|
22
22
|
caller_assertion: str | None = field(default=None, repr=False)
|
|
23
|
+
# The person a requester-bound request works for, verified from its
|
|
24
|
+
# assertion; None on any other request. The user stays the caller, the
|
|
25
|
+
# acting application (ADR-0036, requester-bound calls).
|
|
26
|
+
requester: Any = None
|
|
23
27
|
|
|
24
28
|
|
|
25
29
|
_current_scope: ContextVar[_RequestScope | None] = ContextVar(
|
|
@@ -42,17 +46,27 @@ def _request_scope():
|
|
|
42
46
|
scope.active = False
|
|
43
47
|
scope.user = None
|
|
44
48
|
scope.caller_assertion = None
|
|
49
|
+
scope.requester = None
|
|
45
50
|
_current_scope.reset(token)
|
|
46
51
|
|
|
47
52
|
|
|
48
|
-
def
|
|
53
|
+
def _authenticated_scope(getter: str) -> _RequestScope:
|
|
49
54
|
scope = _current_scope.get()
|
|
50
55
|
if scope is None or not scope.active or scope.user is None:
|
|
51
56
|
raise RequestIdentityError(
|
|
52
57
|
"No authenticated request user is available. Install request identity "
|
|
53
|
-
"once in the application and call
|
|
58
|
+
f"once in the application and call {getter} inside a request."
|
|
54
59
|
)
|
|
55
|
-
return scope
|
|
60
|
+
return scope
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _get_request_identity():
|
|
64
|
+
return _authenticated_scope("User.get_logged_user()").user
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _get_requester():
|
|
68
|
+
"""The requester of the current request; None when it is not requester-bound."""
|
|
69
|
+
return _authenticated_scope("User.get_requester()").requester
|
|
56
70
|
|
|
57
71
|
|
|
58
72
|
@contextmanager
|
|
@@ -63,6 +77,11 @@ def _reads_as_caller():
|
|
|
63
77
|
"Reading as the caller needs an authenticated request. Install request "
|
|
64
78
|
"identity once in the application and call reads_as_caller() inside a request."
|
|
65
79
|
)
|
|
80
|
+
if scope.requester is not None:
|
|
81
|
+
raise RequestIdentityError(
|
|
82
|
+
"This request is requester-bound. The requester's access is not passed on, "
|
|
83
|
+
"so a requester-bound request cannot read as its caller."
|
|
84
|
+
)
|
|
66
85
|
if scope.caller_assertion is None:
|
|
67
86
|
raise RequestIdentityError(
|
|
68
87
|
"This request carries no caller assertion. Reading as the caller needs a "
|
|
@@ -10,6 +10,7 @@ from mainsequence._request_identity import (
|
|
|
10
10
|
RequestIdentityError,
|
|
11
11
|
_caller_read_headers,
|
|
12
12
|
_get_request_identity,
|
|
13
|
+
_get_requester,
|
|
13
14
|
)
|
|
14
15
|
from mainsequence.defaults import STANDARD_BACKEND_URL
|
|
15
16
|
|
|
@@ -30,7 +31,9 @@ class RequestUserIdentity(BaseModel):
|
|
|
30
31
|
"""Minimal identity of the caller of the current runtime request.
|
|
31
32
|
|
|
32
33
|
The caller is a person or a workload identity; ``User.get_by_uid(uid)``
|
|
33
|
-
reads its User.
|
|
34
|
+
reads its User. ``User.get_requester()`` returns the same type for the
|
|
35
|
+
requester of a requester-bound request, the person the calling
|
|
36
|
+
application works for; its ``is_organization_admin`` is always ``False``.
|
|
34
37
|
"""
|
|
35
38
|
|
|
36
39
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
@@ -52,8 +55,9 @@ class RequestUserIdentity(BaseModel):
|
|
|
52
55
|
(),
|
|
53
56
|
title="Team UIDs",
|
|
54
57
|
description=(
|
|
55
|
-
"Canonical UIDs of the caller's active teams,
|
|
56
|
-
"in the caller assertion. Empty when the request carries no
|
|
58
|
+
"Canonical UIDs of the caller's active teams, or the requester's, as signed "
|
|
59
|
+
"by the platform in the caller assertion. Empty when the request carries no "
|
|
60
|
+
"such facts."
|
|
57
61
|
),
|
|
58
62
|
)
|
|
59
63
|
is_organization_admin: bool = Field(
|
|
@@ -61,7 +65,8 @@ class RequestUserIdentity(BaseModel):
|
|
|
61
65
|
title="Organization admin",
|
|
62
66
|
description=(
|
|
63
67
|
"Whether the platform signed the caller as an admin of the application's "
|
|
64
|
-
"Organization. False when the request carries no such facts
|
|
68
|
+
"Organization. False when the request carries no such facts, and always "
|
|
69
|
+
"False for the requester of a requester-bound request."
|
|
65
70
|
),
|
|
66
71
|
)
|
|
67
72
|
|
|
@@ -1299,3 +1304,30 @@ class User(_CallerDirectoryReadMixin, UserApiBaseObjectOrm, BasePydanticModel):
|
|
|
1299
1304
|
if not isinstance(user, RequestUserIdentity):
|
|
1300
1305
|
raise RequestIdentityError("Invalid request user context value.")
|
|
1301
1306
|
return user
|
|
1307
|
+
|
|
1308
|
+
@classmethod
|
|
1309
|
+
def get_requester(cls) -> RequestUserIdentity | None:
|
|
1310
|
+
"""Return the requester of a requester-bound request, or ``None``.
|
|
1311
|
+
|
|
1312
|
+
A requester-bound request comes from another application, for example
|
|
1313
|
+
an Agent, while it works for a person: the requester. The platform
|
|
1314
|
+
signs the requester into the caller assertion. ``get_logged_user()``
|
|
1315
|
+
still returns the caller, the acting application. The requester has
|
|
1316
|
+
the ``team_uids`` the platform signed and ``is_organization_admin``
|
|
1317
|
+
``False``: the requester's access reaches the application at member
|
|
1318
|
+
level only.
|
|
1319
|
+
|
|
1320
|
+
Returns ``None`` when the request is not requester-bound, including
|
|
1321
|
+
local mode and WebSockets. Raises ``RequestIdentityError`` outside an
|
|
1322
|
+
authenticated request, as ``get_logged_user()`` does.
|
|
1323
|
+
|
|
1324
|
+
Authorize a requester-bound request against the requester, and give the
|
|
1325
|
+
acting application no rights of its own unless your policy grants them.
|
|
1326
|
+
An operation that needs a person fails closed when this is ``None``.
|
|
1327
|
+
Never take a person's UID from a request body, header or query
|
|
1328
|
+
parameter instead.
|
|
1329
|
+
"""
|
|
1330
|
+
requester = _get_requester()
|
|
1331
|
+
if requester is not None and not isinstance(requester, RequestUserIdentity):
|
|
1332
|
+
raise RequestIdentityError("Invalid request requester context value.")
|
|
1333
|
+
return requester
|
|
@@ -57,6 +57,14 @@ def _canonical_uid(value: object) -> str:
|
|
|
57
57
|
return canonical
|
|
58
58
|
|
|
59
59
|
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class _VerifiedRequester:
|
|
62
|
+
"""The person a requester-bound call works for, as the platform signed it."""
|
|
63
|
+
|
|
64
|
+
user_uid: str
|
|
65
|
+
team_uids: tuple[str, ...] = ()
|
|
66
|
+
|
|
67
|
+
|
|
60
68
|
@dataclass(frozen=True)
|
|
61
69
|
class AuthenticatedCaller:
|
|
62
70
|
user_uid: str
|
|
@@ -66,25 +74,51 @@ class AuthenticatedCaller:
|
|
|
66
74
|
expires_at: int
|
|
67
75
|
team_uids: tuple[str, ...] = ()
|
|
68
76
|
is_organization_admin: bool = False
|
|
77
|
+
# The requester of a requester-bound call, None on any other call. The
|
|
78
|
+
# fields above stay the caller's: the acting application (ADR-0036).
|
|
79
|
+
_requester: _VerifiedRequester | None = None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _team_uids(value: object, *, owner: str) -> tuple[str, ...]:
|
|
83
|
+
if not isinstance(value, list):
|
|
84
|
+
raise InvalidCallerAssertion(f"{owner} team UIDs are invalid.")
|
|
85
|
+
team_uids = tuple(_canonical_uid(item) for item in value)
|
|
86
|
+
if list(team_uids) != sorted(set(team_uids)):
|
|
87
|
+
raise InvalidCallerAssertion(f"{owner} team UIDs must be sorted and unique.")
|
|
88
|
+
return team_uids
|
|
69
89
|
|
|
70
90
|
|
|
71
91
|
def _caller_facts(payload: dict) -> tuple[tuple[str, ...], bool]:
|
|
72
92
|
facts = set(payload) & CALLER_FACT_CLAIMS
|
|
73
|
-
|
|
93
|
+
# A requester-bound call also carries "requester", which _requester() checks.
|
|
94
|
+
claims = set(payload) - facts - {"requester"}
|
|
95
|
+
if claims != REQUIRED_CLAIMS or facts not in (set(), set(CALLER_FACT_CLAIMS)):
|
|
74
96
|
raise InvalidCallerAssertion("Caller assertion claims are invalid.")
|
|
75
97
|
if not facts:
|
|
76
98
|
return (), False
|
|
77
|
-
|
|
78
|
-
if not isinstance(raw_team_uids, list):
|
|
79
|
-
raise InvalidCallerAssertion("Caller team UIDs are invalid.")
|
|
80
|
-
team_uids = tuple(_canonical_uid(value) for value in raw_team_uids)
|
|
81
|
-
if list(team_uids) != sorted(set(team_uids)):
|
|
82
|
-
raise InvalidCallerAssertion("Caller team UIDs must be sorted and unique.")
|
|
99
|
+
team_uids = _team_uids(payload["team_uids"], owner="Caller")
|
|
83
100
|
if type(payload["is_organization_admin"]) is not bool:
|
|
84
101
|
raise InvalidCallerAssertion("Caller admin flag is invalid.")
|
|
85
102
|
return team_uids, payload["is_organization_admin"]
|
|
86
103
|
|
|
87
104
|
|
|
105
|
+
def _requester(payload: dict) -> _VerifiedRequester | None:
|
|
106
|
+
"""The requester of a requester-bound call: exactly ``sub`` and ``team_uids``.
|
|
107
|
+
|
|
108
|
+
It never carries an admin flag: the requester's access reaches the
|
|
109
|
+
application at member level only.
|
|
110
|
+
"""
|
|
111
|
+
if "requester" not in payload:
|
|
112
|
+
return None
|
|
113
|
+
claim = payload["requester"]
|
|
114
|
+
if not isinstance(claim, dict) or set(claim) != {"sub", "team_uids"}:
|
|
115
|
+
raise InvalidCallerAssertion("Caller assertion requester is invalid.")
|
|
116
|
+
return _VerifiedRequester(
|
|
117
|
+
user_uid=_canonical_uid(claim["sub"]),
|
|
118
|
+
team_uids=_team_uids(claim["team_uids"], owner="Requester"),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
88
122
|
class CallerAssertionVerifier:
|
|
89
123
|
"""Verify the signed actor proof against deployment-owned target values.
|
|
90
124
|
|
|
@@ -231,6 +265,7 @@ class CallerAssertionVerifier:
|
|
|
231
265
|
options={"require": list(REQUIRED_CLAIMS)},
|
|
232
266
|
)
|
|
233
267
|
team_uids, is_organization_admin = _caller_facts(payload)
|
|
268
|
+
requester = _requester(payload)
|
|
234
269
|
if (
|
|
235
270
|
payload["aud"] != f"urn:mainsequence:fapi:{self.release_uid}"
|
|
236
271
|
or _canonical_uid(payload["resource_release_uid"]) != self.release_uid
|
|
@@ -251,6 +286,7 @@ class CallerAssertionVerifier:
|
|
|
251
286
|
expires_at=exp,
|
|
252
287
|
team_uids=team_uids,
|
|
253
288
|
is_organization_admin=is_organization_admin,
|
|
289
|
+
_requester=requester,
|
|
254
290
|
)
|
|
255
291
|
except (InvalidTokenError, KeyError, TypeError, ValueError) as exc:
|
|
256
292
|
raise InvalidCallerAssertion("Caller assertion is invalid.") from exc
|
mainsequence/server/fastapi.py
CHANGED
|
@@ -222,6 +222,12 @@ class _RequestIdentityMiddleware:
|
|
|
222
222
|
state["resource_release_uid"] = caller.release_uid
|
|
223
223
|
state["organization_environment_uid"] = caller.environment_uid
|
|
224
224
|
context.caller_assertion = assertion
|
|
225
|
+
if caller._requester is not None:
|
|
226
|
+
# Never an admin: the requester's access is member level.
|
|
227
|
+
context.requester = RequestUserIdentity(
|
|
228
|
+
uid=caller._requester.user_uid,
|
|
229
|
+
team_uids=caller._requester.team_uids,
|
|
230
|
+
)
|
|
225
231
|
else:
|
|
226
232
|
user = await asyncio.to_thread(_local_user, scope)
|
|
227
233
|
context.user = user
|
|
@@ -245,7 +251,11 @@ class _RequestIdentityMiddleware:
|
|
|
245
251
|
|
|
246
252
|
|
|
247
253
|
def install_request_identity(app):
|
|
248
|
-
"""Install once at application creation.
|
|
254
|
+
"""Install once at application creation.
|
|
255
|
+
|
|
256
|
+
Handlers use User.get_logged_user() for the caller and, on a
|
|
257
|
+
requester-bound call, User.get_requester() for the person it works for.
|
|
258
|
+
"""
|
|
249
259
|
if getattr(app.state, _INSTALLATION, None) is not None:
|
|
250
260
|
raise RuntimeError("Request identity is already installed.")
|
|
251
261
|
mode = _mode()
|
|
@@ -278,7 +288,9 @@ def reads_as_caller():
|
|
|
278
288
|
and the platform answers with what the caller may see. No other call sends
|
|
279
289
|
it. Raises ``RequestIdentityError`` outside an authenticated request or when
|
|
280
290
|
the request carries no assertion (local mode, WebSockets); it never reads as
|
|
281
|
-
the application instead.
|
|
291
|
+
the application instead. It also raises in a requester-bound request
|
|
292
|
+
(``User.get_requester()`` is not ``None``): the requester's access is not
|
|
293
|
+
passed on. See ADR-0036.
|
|
282
294
|
"""
|
|
283
295
|
with _reads_as_caller():
|
|
284
296
|
yield
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
agent_scaffold/AGENTS.md,sha256
|
|
1
|
+
agent_scaffold/AGENTS.md,sha256=-tK1zUP6j3QAlFXmwFVSNPfbzchZRJSzj26L-RU9Gis,6471
|
|
2
2
|
agent_scaffold/skills/a2a_sdk_execution/SKILL.md,sha256=NKtcXibE8TxZzKzz1KzJ2cVZ_EeuJJkb660U6wLvZ3s,11809
|
|
3
|
-
agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md,sha256=
|
|
3
|
+
agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md,sha256=HODSJCgm6Wdpd-nBsbMCb4Ls67P7DqK-j_S4GLFdfIk,19087
|
|
4
4
|
agent_scaffold/skills/mainsequence-inference/SKILL.md,sha256=yv8aAHLQ2FBBSWPIFadY7pGNTwtR-yiR_-UzaGRN9-g,12502
|
|
5
5
|
agent_scaffold/skills/maintenance/bug_auditor/SKILL.md,sha256=CCKs7edyZ9RK-WtAgXJXucokJFXZxk8sRUe0_imFtu8,8610
|
|
6
6
|
agent_scaffold/skills/maintenance/code_repository_maintenance/SKILL.md,sha256=pPsZaERlfokSJ6b6vLjpA5U0KuKxbOFzzcxOVW_ikiI,17935
|
|
7
7
|
agent_scaffold/skills/maintenance/metatables_transition/SKILL.md,sha256=PisgpytcksJnfZMS6aiPCmahsT3fm8DIwk_cefXA2qU,3759
|
|
8
|
-
agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md,sha256=
|
|
8
|
+
agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md,sha256=Y3V5EJtA2K7BvvAg6mPLZn1nYcMH4N0fKpmdelfbpa0,12626
|
|
9
9
|
agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md,sha256=_84nnJKl_w-JrcbiTIairbFf9ptoQvW5Y1kMxn_XzkU,16871
|
|
10
10
|
agent_scaffold/skills/sdk_code_repository_execution/SKILL.md,sha256=zly7MheB-Sjd0x6l_oPCOTBqkL2E4SSXwTlfSwdIPfQ,10196
|
|
11
11
|
mainsequence/__init__.py,sha256=70CbIo7ZG1mzi5uwkqDtVESunBE0RanHjeFg4ptfsA0,385
|
|
12
12
|
mainsequence/__main__.py,sha256=KuAP69uduS2ImrPIgNMzbGBRepZzjqvUONu5vFh4v4A,176
|
|
13
|
-
mainsequence/_request_identity.py,sha256=
|
|
13
|
+
mainsequence/_request_identity.py,sha256=YsrfCRzIawYbvjn-DPFqBtjVbD_ujVFttjXpOwP8WKM,4189
|
|
14
14
|
mainsequence/bootstrap.py,sha256=AvG55Y46RcshVqJZsyqvUDN62CJg4CDbHu0Bvsbwfzs,2910
|
|
15
15
|
mainsequence/code_repository_context.py,sha256=JRrWjwpnUy8I-OjSd9hHlrcq8-Ow3Zeh_N6cYvmtA3o,28545
|
|
16
16
|
mainsequence/code_repository_skills.py,sha256=AyC1HFAxQXHXNBGbarVUJS2cHd4X5HwDx6u8XBophbE,23611
|
|
@@ -45,18 +45,18 @@ 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=nlbHuQAylTOHAZOq4ltJQp6QvC3NW_kw7tZFosGc5rM,47311
|
|
48
|
-
mainsequence/client/models_user.py,sha256=
|
|
48
|
+
mainsequence/client/models_user.py,sha256=tOngVHxkoEeV-kLLK3cmfESN9gzyXrLlZHFNGVv014Q,45241
|
|
49
49
|
mainsequence/client/observability.py,sha256=_WrDKCaNk_TXmKElKCXC1inork6Exxxe8FRY4m1GI9s,16008
|
|
50
50
|
mainsequence/client/utils.py,sha256=C0h0U-fOBZw3UxIwHcBTLf8CDClAOatf_oRgjvqFth4,31637
|
|
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=
|
|
57
|
-
mainsequence-9.0.
|
|
58
|
-
mainsequence-9.0.
|
|
59
|
-
mainsequence-9.0.
|
|
60
|
-
mainsequence-9.0.
|
|
61
|
-
mainsequence-9.0.
|
|
62
|
-
mainsequence-9.0.
|
|
55
|
+
mainsequence/server/caller_assertions.py,sha256=Ix94Kvei31lKb861q6LqKzcB5H1mWnuH_wYwpXEKS1E,12351
|
|
56
|
+
mainsequence/server/fastapi.py,sha256=1l5uIx4jqw092NJwXaA57nI6sAt9a_zXzJKgD4I1NRk,12090
|
|
57
|
+
mainsequence-9.0.15.dev57.dist-info/licenses/LICENSE,sha256=b6Ecsk8wjsEby3lbSzdljR6hHiFGHBizt9iWV2p1c_I,1074
|
|
58
|
+
mainsequence-9.0.15.dev57.dist-info/METADATA,sha256=AxjdXTfjT_TVHTrYRdQljivI9c15SpW9jhtZWyKUzgc,7403
|
|
59
|
+
mainsequence-9.0.15.dev57.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
60
|
+
mainsequence-9.0.15.dev57.dist-info/entry_points.txt,sha256=XGAOq3-mF2lWWpeRJvoaWyMarRcK167mH1g124aGdUs,88
|
|
61
|
+
mainsequence-9.0.15.dev57.dist-info/top_level.txt,sha256=BFeblM_Q9q0cUWBvTxnfWcWQEHtf5XZi9nOS-Gdfwus,28
|
|
62
|
+
mainsequence-9.0.15.dev57.dist-info/RECORD,,
|
|
File without changes
|
{mainsequence-9.0.14.dev56.dist-info → mainsequence-9.0.15.dev57.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{mainsequence-9.0.14.dev56.dist-info → mainsequence-9.0.15.dev57.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|