mainsequence 9.0.17.dev60__tar.gz → 9.0.18.dev61__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.
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/PKG-INFO +1 -1
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +42 -32
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +32 -31
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/client/models_user.py +37 -30
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/server/caller_assertions.py +28 -21
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/server/fastapi.py +6 -4
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence.egg-info/PKG-INFO +1 -1
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/pyproject.toml +1 -1
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_caller_assertions.py +107 -49
- mainsequence-9.0.18.dev61/tests/test_fastapi_mcp.py +636 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_fastapi_request_identity.py +117 -37
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_models_user_request_bound_auth.py +6 -2
- mainsequence-9.0.17.dev60/tests/test_fastapi_mcp.py +0 -380
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/LICENSE +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/README.md +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/agent_scaffold/AGENTS.md +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/agent_scaffold/skills/a2a_sdk_execution/SKILL.md +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/agent_scaffold/skills/mainsequence-inference/SKILL.md +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/agent_scaffold/skills/maintenance/code_repository_maintenance/SKILL.md +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/agent_scaffold/skills/maintenance/metatables_transition/SKILL.md +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/agent_scaffold/skills/sdk_code_repository_execution/SKILL.md +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/__init__.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/__main__.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/_request_identity.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/bootstrap.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/cli/__init__.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/cli/api.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/cli/browser_auth.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/cli/cli.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/cli/code_repository_status.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/cli/config.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/cli/docker_utils.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/cli/doctor.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/cli/local_ops.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/cli/model_filters.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/cli/pydantic_cli.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/cli/sdk_utils.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/cli/ssh_utils.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/cli/ui.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/client/__init__.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/client/_transport.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/client/agent_runtime_models.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/client/base.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/client/client.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/client/compute_validation.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/client/exceptions.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/client/github_issues.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/client/inference.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/client/models_foundry.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/client/models_helpers.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/client/observability.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/client/utils.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/client/value_sets.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/code_repository_context.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/code_repository_skills.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/defaults.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/instrumentation/__init__.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/instrumentation/utils.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/logconf.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/repository_identity_security.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/runtime_credential_exchange.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/runtime_flags.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/scaffold_skills.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/server/__init__.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence.egg-info/SOURCES.txt +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence.egg-info/dependency_links.txt +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence.egg-info/entry_points.txt +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence.egg-info/requires.txt +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence.egg-info/top_level.txt +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/setup.cfg +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_auth_bootstrap.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_auth_precedence.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_canonical_api_route_contract.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_cli_auth_contract.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_cli_browser_auth.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_cli_secure_storage.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_client.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_code_repository_branch_contract.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_code_repository_documentation_contract.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_code_repository_runtime_context.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_code_repository_skills.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_credential_repr.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_dependency_extras.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_dev_release_version.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_environment_resource_context.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_environment_response_contracts.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_github_issues_client.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_http_retry_budget.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_independent_context.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_inference_client.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_instrumentation.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_job_client.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_local_ops.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_logconf.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_notification_uid_contract.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_observability_client.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_runtime_credential_exchange.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_scaffold_skills.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_secret_client_model.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_source_ownership.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_ssh_utils.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_tolerant_response_reading.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_tolerant_value_sets.py +0 -0
- {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_user_application_facts.py +0 -0
|
@@ -164,9 +164,16 @@ for the lifecycle and trust contract.
|
|
|
164
164
|
|
|
165
165
|
Another application, for example an Agent, can call the API while it works for
|
|
166
166
|
a person: the requester. The platform signs the requester into the caller
|
|
167
|
-
assertion it sends to the API
|
|
168
|
-
|
|
169
|
-
|
|
167
|
+
assertion it sends to the API, and the SDK verifies it before the handler runs.
|
|
168
|
+
`User.get_logged_user()` still returns the caller, the acting application.
|
|
169
|
+
`User.get_requester()` returns the requester, the checked delegation, with the
|
|
170
|
+
person's own `uid`, `team_uids` and `is_organization_admin`, or `None` when the
|
|
171
|
+
call carries no delegation. A requester-bound call carries the person's
|
|
172
|
+
ordinary permissions, including administrative ones.
|
|
173
|
+
|
|
174
|
+
Each handler decides whether it needs a person. One that needs a person fails
|
|
175
|
+
with its own error when there is none; one where the person is optional
|
|
176
|
+
authorizes against the person when present, and against the caller otherwise:
|
|
170
177
|
|
|
171
178
|
```python
|
|
172
179
|
from fastapi import HTTPException
|
|
@@ -184,41 +191,42 @@ def require_requester() -> RequestUserIdentity:
|
|
|
184
191
|
@app.get("/team-reports/{team_uid}")
|
|
185
192
|
def team_report(team_uid: str) -> dict[str, str]:
|
|
186
193
|
requester = require_requester()
|
|
187
|
-
if team_uid
|
|
188
|
-
raise HTTPException(status_code=403, detail="The requester
|
|
194
|
+
if not (requester.is_organization_admin or team_uid in requester.team_uids):
|
|
195
|
+
raise HTTPException(status_code=403, detail="The requester cannot read this report.")
|
|
189
196
|
return {"team_uid": team_uid, "requested_by": requester.uid}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def acting_identity() -> RequestUserIdentity:
|
|
200
|
+
"""The person on a requester-bound call, the caller on any other; never both."""
|
|
201
|
+
return User.get_requester() or User.get_logged_user()
|
|
190
202
|
```
|
|
191
203
|
|
|
192
|
-
- Authorize a requester-bound call
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
verified that it is addressed to this release.
|
|
204
|
-
- The requester carries the `team_uids` the platform signed, and its
|
|
205
|
-
`is_organization_admin` is always `False`: a requester's access reaches the
|
|
206
|
-
API at member level only.
|
|
204
|
+
- Authorize against one identity: the person on a requester-bound call, the
|
|
205
|
+
caller on any other. Never combine the caller's grants with the person's,
|
|
206
|
+
and never fall back to the caller's rights when the person may not.
|
|
207
|
+
- Before a write, check the exact object and action; the example's
|
|
208
|
+
team-membership check is not a general edit, run, share or delete grant. The
|
|
209
|
+
getter verifies identity, not operation permission.
|
|
210
|
+
- Never parse the assertion, headers or MCP `_meta`, never read a person's UID
|
|
211
|
+
from the request body, a header or a query parameter, and never let a caller
|
|
212
|
+
name whom it acts for. Anyone can write a UID there. Only the signed
|
|
213
|
+
assertion names the requester, and the SDK has verified that it is addressed
|
|
214
|
+
to this release; an invalid one is rejected with 401 before any handler runs.
|
|
207
215
|
- Inside a requester-bound call, `reads_as_caller()` raises
|
|
208
216
|
`RequestIdentityError`: a receiving application cannot forward its inbound
|
|
209
217
|
requester-bearing assertion to the directory endpoints. Platform-authorized
|
|
210
218
|
Agent delegation is separate and keeps the original person and request time.
|
|
211
|
-
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
write endpoints or extend the inbound request scope.
|
|
219
|
+
- The platform bounds work to at most 24 hours after the original request and
|
|
220
|
+
checks access on every call; delegation does not restart that limit. This
|
|
221
|
+
SDK does not enable platform endpoints or extend the inbound request scope.
|
|
215
222
|
- A model-driven Agent can be steered by prompt injection. Its writes can
|
|
216
223
|
cause damage within the person's permissions; keep object/action checks and
|
|
217
224
|
any mutation approval required by the application's policy outside model
|
|
218
225
|
claims or tool arguments.
|
|
219
226
|
- Test requester-authorized reads and writes, writes refused for a requester
|
|
220
|
-
without the necessary grant,
|
|
221
|
-
|
|
227
|
+
without the necessary grant, requester-required operations refused without
|
|
228
|
+
a requester, and writes the calling Agent may make but the person may not
|
|
229
|
+
refused on a requester-bound call.
|
|
222
230
|
|
|
223
231
|
Which applications may act for their requester, what that access covers, and
|
|
224
232
|
what people are told:
|
|
@@ -233,12 +241,14 @@ To serve MCP tools from the same application, add the `mcp` extra and call
|
|
|
233
241
|
after `install_request_identity(app)`, with a stateless FastMCP server that
|
|
234
242
|
returns JSON. Tools authorize with `User.get_logged_user()` and
|
|
235
243
|
`User.get_requester()` exactly like REST handlers; admission to the release
|
|
236
|
-
does not authorize every tool.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
244
|
+
does not authorize every tool. Each tool decides whether it needs a person: a
|
|
245
|
+
tool that needs one fails with its own error when the getter returns `None`,
|
|
246
|
+
and a tool where the person is optional authorizes against the person when
|
|
247
|
+
present and the caller otherwise, never both. Verify object- and
|
|
248
|
+
action-specific permission before mutation; read access and MCP annotations
|
|
249
|
+
are not write authority. The identity reaches tasks a tool starts and worker
|
|
250
|
+
threads it uses through `asyncio.to_thread` or anyio's `to_thread.run_sync`,
|
|
251
|
+
and ends with the call. See
|
|
242
252
|
[Serving MCP](https://mainsequence-sdk.github.io/mainsequence-sdk/knowledge/fastapi/#serving-mcp).
|
|
243
253
|
|
|
244
254
|
### Public provider callbacks and webhooks
|
|
@@ -240,13 +240,17 @@ whose own request it is serving. The platform setting is `acts_for_requester`.
|
|
|
240
240
|
|
|
241
241
|
### What the requester's access covers
|
|
242
242
|
|
|
243
|
-
|
|
244
|
-
|
|
243
|
+
The platform applies one rule to every call an Agent makes for its work: with
|
|
244
|
+
no delegation, the caller's own ordinary permissions; with a valid delegation,
|
|
245
|
+
the person's ordinary permissions, including administrative ones; an invalid or
|
|
246
|
+
expired delegation is rejected, never switched to another identity.
|
|
247
|
+
|
|
248
|
+
- The reads, creates, updates, runs, sharing changes and deletes the person
|
|
249
|
+
may make through supported operations.
|
|
245
250
|
- Check the person's permission for the exact object and operation on every
|
|
246
251
|
call. A read grant or team membership alone does not authorize a write.
|
|
247
|
-
-
|
|
248
|
-
|
|
249
|
-
- Hosted Agents never receive Secret values, including through this access.
|
|
252
|
+
- Only the person's permissions: never the acting Agent's grants, and never
|
|
253
|
+
the two combined.
|
|
250
254
|
- At most 24 hours after the person's request, and only while the work for that
|
|
251
255
|
request runs.
|
|
252
256
|
- Checked again on every call, so it stops the moment the person's access ends.
|
|
@@ -257,42 +261,38 @@ whose own request it is serving. The platform setting is `acts_for_requester`.
|
|
|
257
261
|
- A receiving application cannot forward its inbound assertion to establish
|
|
258
262
|
another requester binding. `reads_as_caller()` still refuses these requests.
|
|
259
263
|
|
|
260
|
-
|
|
261
|
-
not enable
|
|
262
|
-
application's own grants.
|
|
264
|
+
The platform decides which operations support requester-bound access;
|
|
265
|
+
installing this SDK does not enable one. Treat a refused operation as refused,
|
|
266
|
+
never retry it with the application's own grants.
|
|
263
267
|
|
|
264
268
|
### Never accept a person's UID from a request
|
|
265
269
|
|
|
266
270
|
The requester comes only from the platform. An application never accepts a
|
|
267
|
-
person's UID from a request body, header or query parameter,
|
|
268
|
-
|
|
269
|
-
requester-bound call reads
|
|
270
|
-
|
|
271
|
-
|
|
271
|
+
person's UID from a request body, header or query parameter, never parses the
|
|
272
|
+
assertion, headers or MCP `_meta` itself, and never lets a caller choose whom
|
|
273
|
+
it acts for. A FastAPI application that receives a requester-bound call reads
|
|
274
|
+
the requester with `User.get_requester()`, the person's own `uid`, `team_uids`
|
|
275
|
+
and `is_organization_admin`, and authorizes reads and writes against that
|
|
276
|
+
person's object- and operation-specific permissions. Each handler or tool
|
|
277
|
+
decides whether it needs a person. See
|
|
272
278
|
`.agents/skills/mainsequence/application_surfaces/api_surfaces/SKILL.md`.
|
|
273
279
|
|
|
274
280
|
### What people are told
|
|
275
281
|
|
|
276
|
-
Every client that shows an application with `acts_for_requester`
|
|
277
|
-
platform
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
deployments, the approved wording must cover supported reads and writes within
|
|
281
|
-
the person's member-level permissions, Organization-admin approval without
|
|
282
|
-
admin powers or Secret values, the original 24-hour limit including delegation,
|
|
283
|
-
access revocation, and prompt-injection risk. Never present a write-enabled
|
|
284
|
-
Agent as read-only or claim that it cannot change, share or delete anything.
|
|
282
|
+
Every client that shows an application with `acts_for_requester` shows the
|
|
283
|
+
platform's statement, exactly:
|
|
284
|
+
|
|
285
|
+
> **This Agent works with your identity.** It can read, create, change, run, share or delete only what your permissions allow through supported operations, only while serving your request, and for at most 24 hours after you ask. It uses your ordinary permissions, including administrative permissions, and access is checked on every call. Your Organization's administrator approved it to work this way.
|
|
285
286
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
the disclosure is ready for a write-enabled rollout or invent replacement words.
|
|
287
|
+
Do not invent, paraphrase or shorten it. Never present such an Agent as
|
|
288
|
+
read-only or claim that it cannot change, share or delete anything.
|
|
289
289
|
|
|
290
290
|
Explain the risk alongside the statement: a model-driven Agent can be steered
|
|
291
291
|
by prompt injection. Its reads and writes can reach as far as the person's
|
|
292
|
-
|
|
293
|
-
supported, for at most 24 hours after the original
|
|
294
|
-
admins decide which Agents may receive this
|
|
295
|
-
per-operation permission checks remain necessary.
|
|
292
|
+
ordinary permissions allow, administrative ones included, such as sharing and
|
|
293
|
+
deleting through supported operations, for at most 24 hours after the original
|
|
294
|
+
request. Only Organization admins decide which Agents may receive this
|
|
295
|
+
authority; the application's per-operation permission checks remain necessary.
|
|
296
296
|
|
|
297
297
|
## Review Rules
|
|
298
298
|
|
|
@@ -311,7 +311,8 @@ When reviewing an access-control task, look for:
|
|
|
311
311
|
- an application that takes a person's UID from a request instead of from the
|
|
312
312
|
platform
|
|
313
313
|
- `acts_for_requester` presented as something a non-admin can enable
|
|
314
|
-
- a requester-bound write authorized
|
|
314
|
+
- a requester-bound write authorized by a read grant, by the Agent's grants, or
|
|
315
|
+
by the Agent's grants combined with the person's
|
|
315
316
|
- prompt-injection risk hidden behind a claim that requester access is read-only
|
|
316
317
|
|
|
317
318
|
## Validation Checklist
|
|
@@ -332,7 +333,7 @@ Do not claim success until you have checked:
|
|
|
332
333
|
- an application that acts for its requester takes the person only from the
|
|
333
334
|
platform, and its own grants are intentional
|
|
334
335
|
- requester-bound writes enforce the person's permission for the operation,
|
|
335
|
-
and clients
|
|
336
|
+
and clients show the platform's exact statement and explain the write risk
|
|
336
337
|
|
|
337
338
|
## This Skill Must Stop And Escalate When
|
|
338
339
|
|
|
@@ -33,7 +33,8 @@ class RequestUserIdentity(BaseModel):
|
|
|
33
33
|
The caller is a person or a workload identity; ``User.get_by_uid(uid)``
|
|
34
34
|
reads its User. ``User.get_requester()`` returns the same type for the
|
|
35
35
|
requester of a requester-bound request, the person the calling
|
|
36
|
-
application works for
|
|
36
|
+
application works for, with that person's own ``team_uids`` and
|
|
37
|
+
``is_organization_admin``.
|
|
37
38
|
"""
|
|
38
39
|
|
|
39
40
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
@@ -55,18 +56,18 @@ class RequestUserIdentity(BaseModel):
|
|
|
55
56
|
(),
|
|
56
57
|
title="Team UIDs",
|
|
57
58
|
description=(
|
|
58
|
-
"Canonical UIDs of the
|
|
59
|
-
"
|
|
60
|
-
"such facts."
|
|
59
|
+
"Canonical UIDs of the active teams the platform signed in the caller "
|
|
60
|
+
"assertion: the caller's, or for the requester the person's own. Empty in "
|
|
61
|
+
"local mode and on WebSockets, which carry no such facts."
|
|
61
62
|
),
|
|
62
63
|
)
|
|
63
64
|
is_organization_admin: bool = Field(
|
|
64
65
|
False,
|
|
65
66
|
title="Organization admin",
|
|
66
67
|
description=(
|
|
67
|
-
"Whether the platform signed
|
|
68
|
-
"Organization
|
|
69
|
-
"
|
|
68
|
+
"Whether the platform signed this identity as an admin of the application's "
|
|
69
|
+
"Organization: the caller, or for the requester the person. False in local "
|
|
70
|
+
"mode and on WebSockets, which carry no such facts."
|
|
70
71
|
),
|
|
71
72
|
)
|
|
72
73
|
|
|
@@ -1307,34 +1308,40 @@ class User(_CallerDirectoryReadMixin, UserApiBaseObjectOrm, BasePydanticModel):
|
|
|
1307
1308
|
|
|
1308
1309
|
@classmethod
|
|
1309
1310
|
def get_requester(cls) -> RequestUserIdentity | None:
|
|
1310
|
-
"""Return the
|
|
1311
|
+
"""Return the person a requester-bound request works for, or ``None``.
|
|
1311
1312
|
|
|
1312
1313
|
A requester-bound request comes from another application, for example
|
|
1313
1314
|
an Agent, while it works for a person: the requester. The platform
|
|
1314
|
-
signs
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
``
|
|
1318
|
-
|
|
1315
|
+
signs that person into the caller assertion, and the request identity
|
|
1316
|
+
integration verifies it before any handler or tool runs. The result is
|
|
1317
|
+
that checked delegation: the person's own ``uid``, ``team_uids`` and
|
|
1318
|
+
``is_organization_admin``, as the platform signed them.
|
|
1319
|
+
``get_logged_user()`` still returns the caller, the acting application.
|
|
1319
1320
|
|
|
1320
|
-
Returns ``None`` when the request
|
|
1321
|
+
Returns ``None`` when the request carries no delegation, including
|
|
1321
1322
|
local mode and WebSockets. Raises ``RequestIdentityError`` outside an
|
|
1322
|
-
authenticated request, as ``get_logged_user()`` does.
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1323
|
+
authenticated request, as ``get_logged_user()`` does. An invalid
|
|
1324
|
+
delegation rejects the whole request before any handler or tool runs;
|
|
1325
|
+
it never falls back to the caller.
|
|
1326
|
+
|
|
1327
|
+
Each handler or tool decides whether it needs a person:
|
|
1328
|
+
|
|
1329
|
+
- one that needs a person fails with its own error when this returns
|
|
1330
|
+
``None``;
|
|
1331
|
+
- one where the person is optional authorizes against this result when
|
|
1332
|
+
present, and against ``get_logged_user()`` otherwise;
|
|
1333
|
+
- none parses assertions, headers or MCP ``_meta``, takes a person's
|
|
1334
|
+
UID from a request body, header or query parameter, or combines the
|
|
1335
|
+
caller's grants with the person's.
|
|
1336
|
+
|
|
1337
|
+
Authorize against the person's ordinary permissions, including
|
|
1338
|
+
administrative ones, for the exact object and operation. This getter
|
|
1339
|
+
verifies identity, not permission; read access or team membership does
|
|
1340
|
+
not authorize a write. The platform checks access on every call and
|
|
1341
|
+
limits the work to at most 24 hours after the person's request. A
|
|
1342
|
+
model-driven Agent can be steered by prompt injection, so per-operation
|
|
1343
|
+
checks remain necessary. This getter does not enable platform
|
|
1344
|
+
endpoints or extend its request scope.
|
|
1338
1345
|
"""
|
|
1339
1346
|
requester = _get_requester()
|
|
1340
1347
|
if requester is not None and not isinstance(requester, RequestUserIdentity):
|
{mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/server/caller_assertions.py
RENAMED
|
@@ -32,12 +32,14 @@ REQUIRED_CLAIMS = frozenset(
|
|
|
32
32
|
"iat",
|
|
33
33
|
"nbf",
|
|
34
34
|
"exp",
|
|
35
|
+
# What the platform states about the caller: its active team UIDs and
|
|
36
|
+
# whether it is an admin of the application's Organization.
|
|
37
|
+
"team_uids",
|
|
38
|
+
"is_organization_admin",
|
|
35
39
|
}
|
|
36
40
|
)
|
|
37
|
-
#
|
|
38
|
-
|
|
39
|
-
# predates them; then the caller has no teams and is not an admin.
|
|
40
|
-
CALLER_FACT_CLAIMS = frozenset({"team_uids", "is_organization_admin"})
|
|
41
|
+
# The person a requester-bound call works for, with the person's own facts.
|
|
42
|
+
REQUESTER_CLAIMS = frozenset({"sub", "team_uids", "is_organization_admin"})
|
|
41
43
|
|
|
42
44
|
|
|
43
45
|
class InvalidCallerAssertion(ValueError):
|
|
@@ -62,7 +64,8 @@ class _VerifiedRequester:
|
|
|
62
64
|
"""The person a requester-bound call works for, as the platform signed it."""
|
|
63
65
|
|
|
64
66
|
user_uid: str
|
|
65
|
-
team_uids: tuple[str, ...]
|
|
67
|
+
team_uids: tuple[str, ...]
|
|
68
|
+
is_organization_admin: bool
|
|
66
69
|
|
|
67
70
|
|
|
68
71
|
@dataclass(frozen=True)
|
|
@@ -72,9 +75,9 @@ class AuthenticatedCaller:
|
|
|
72
75
|
environment_uid: str
|
|
73
76
|
issued_at: int
|
|
74
77
|
expires_at: int
|
|
75
|
-
team_uids: tuple[str, ...]
|
|
76
|
-
is_organization_admin: bool
|
|
77
|
-
# The
|
|
78
|
+
team_uids: tuple[str, ...]
|
|
79
|
+
is_organization_admin: bool
|
|
80
|
+
# The person a requester-bound call works for, None on any other call. The
|
|
78
81
|
# fields above stay the caller's: the acting application (ADR-0036).
|
|
79
82
|
_requester: _VerifiedRequester | None = None
|
|
80
83
|
|
|
@@ -88,34 +91,38 @@ def _team_uids(value: object, *, owner: str) -> tuple[str, ...]:
|
|
|
88
91
|
return team_uids
|
|
89
92
|
|
|
90
93
|
|
|
94
|
+
def _admin_flag(value: object, *, owner: str) -> bool:
|
|
95
|
+
if type(value) is not bool:
|
|
96
|
+
raise InvalidCallerAssertion(f"{owner} admin flag is invalid.")
|
|
97
|
+
return value
|
|
98
|
+
|
|
99
|
+
|
|
91
100
|
def _caller_facts(payload: dict) -> tuple[tuple[str, ...], bool]:
|
|
92
|
-
|
|
101
|
+
"""The caller's team UIDs and admin flag, which every assertion carries."""
|
|
93
102
|
# A requester-bound call also carries "requester", which _requester() checks.
|
|
94
|
-
|
|
95
|
-
if claims != REQUIRED_CLAIMS or facts not in (set(), set(CALLER_FACT_CLAIMS)):
|
|
103
|
+
if set(payload) - {"requester"} != REQUIRED_CLAIMS:
|
|
96
104
|
raise InvalidCallerAssertion("Caller assertion claims are invalid.")
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
raise InvalidCallerAssertion("Caller admin flag is invalid.")
|
|
102
|
-
return team_uids, payload["is_organization_admin"]
|
|
105
|
+
return (
|
|
106
|
+
_team_uids(payload["team_uids"], owner="Caller"),
|
|
107
|
+
_admin_flag(payload["is_organization_admin"], owner="Caller"),
|
|
108
|
+
)
|
|
103
109
|
|
|
104
110
|
|
|
105
111
|
def _requester(payload: dict) -> _VerifiedRequester | None:
|
|
106
|
-
"""The
|
|
112
|
+
"""The person a requester-bound call works for, or None on any other call.
|
|
107
113
|
|
|
108
|
-
|
|
109
|
-
|
|
114
|
+
The claim has exactly ``sub``, ``team_uids`` and ``is_organization_admin``:
|
|
115
|
+
the person's own User UID, active team UIDs and Organization-admin flag.
|
|
110
116
|
"""
|
|
111
117
|
if "requester" not in payload:
|
|
112
118
|
return None
|
|
113
119
|
claim = payload["requester"]
|
|
114
|
-
if not isinstance(claim, dict) or set(claim) !=
|
|
120
|
+
if not isinstance(claim, dict) or set(claim) != REQUESTER_CLAIMS:
|
|
115
121
|
raise InvalidCallerAssertion("Caller assertion requester is invalid.")
|
|
116
122
|
return _VerifiedRequester(
|
|
117
123
|
user_uid=_canonical_uid(claim["sub"]),
|
|
118
124
|
team_uids=_team_uids(claim["team_uids"], owner="Requester"),
|
|
125
|
+
is_organization_admin=_admin_flag(claim["is_organization_admin"], owner="Requester"),
|
|
119
126
|
)
|
|
120
127
|
|
|
121
128
|
|
|
@@ -229,10 +229,11 @@ class _RequestIdentityMiddleware:
|
|
|
229
229
|
state["organization_environment_uid"] = caller.environment_uid
|
|
230
230
|
context.caller_assertion = assertion
|
|
231
231
|
if caller._requester is not None:
|
|
232
|
-
#
|
|
232
|
+
# The person this call works for, with the person's own facts.
|
|
233
233
|
context.requester = RequestUserIdentity(
|
|
234
234
|
uid=caller._requester.user_uid,
|
|
235
235
|
team_uids=caller._requester.team_uids,
|
|
236
|
+
is_organization_admin=caller._requester.is_organization_admin,
|
|
236
237
|
)
|
|
237
238
|
else:
|
|
238
239
|
user = await asyncio.to_thread(_local_user, scope)
|
|
@@ -397,9 +398,10 @@ def install_mcp(app, mcp_app, *, lifespan):
|
|
|
397
398
|
|
|
398
399
|
Install request identity first. Tools read the caller with
|
|
399
400
|
``User.get_logged_user()`` and, on a requester-bound call, the person it
|
|
400
|
-
works for with ``User.get_requester()
|
|
401
|
-
|
|
402
|
-
|
|
401
|
+
works for with ``User.get_requester()``; each tool decides whether it needs
|
|
402
|
+
that person. Only authenticated POSTs reach the server; other methods
|
|
403
|
+
answer 405 and an ``Origin`` other than the release's own or its CORS
|
|
404
|
+
origins answers 403. See ADR 0038.
|
|
403
405
|
"""
|
|
404
406
|
from starlette.routing import Route
|
|
405
407
|
|