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.
Files changed (106) hide show
  1. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/PKG-INFO +1 -1
  2. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +42 -32
  3. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +32 -31
  4. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/client/models_user.py +37 -30
  5. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/server/caller_assertions.py +28 -21
  6. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/server/fastapi.py +6 -4
  7. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence.egg-info/PKG-INFO +1 -1
  8. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/pyproject.toml +1 -1
  9. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_caller_assertions.py +107 -49
  10. mainsequence-9.0.18.dev61/tests/test_fastapi_mcp.py +636 -0
  11. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_fastapi_request_identity.py +117 -37
  12. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_models_user_request_bound_auth.py +6 -2
  13. mainsequence-9.0.17.dev60/tests/test_fastapi_mcp.py +0 -380
  14. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/LICENSE +0 -0
  15. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/README.md +0 -0
  16. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/agent_scaffold/AGENTS.md +0 -0
  17. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/agent_scaffold/skills/a2a_sdk_execution/SKILL.md +0 -0
  18. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/agent_scaffold/skills/mainsequence-inference/SKILL.md +0 -0
  19. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
  20. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/agent_scaffold/skills/maintenance/code_repository_maintenance/SKILL.md +0 -0
  21. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/agent_scaffold/skills/maintenance/metatables_transition/SKILL.md +0 -0
  22. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
  23. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/agent_scaffold/skills/sdk_code_repository_execution/SKILL.md +0 -0
  24. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/__init__.py +0 -0
  25. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/__main__.py +0 -0
  26. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/_request_identity.py +0 -0
  27. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/bootstrap.py +0 -0
  28. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/cli/__init__.py +0 -0
  29. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/cli/api.py +0 -0
  30. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/cli/browser_auth.py +0 -0
  31. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/cli/cli.py +0 -0
  32. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/cli/code_repository_status.py +0 -0
  33. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/cli/config.py +0 -0
  34. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/cli/docker_utils.py +0 -0
  35. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/cli/doctor.py +0 -0
  36. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/cli/local_ops.py +0 -0
  37. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/cli/model_filters.py +0 -0
  38. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/cli/pydantic_cli.py +0 -0
  39. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/cli/sdk_utils.py +0 -0
  40. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/cli/ssh_utils.py +0 -0
  41. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/cli/ui.py +0 -0
  42. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/client/__init__.py +0 -0
  43. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/client/_transport.py +0 -0
  44. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/client/agent_runtime_models.py +0 -0
  45. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/client/base.py +0 -0
  46. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/client/client.py +0 -0
  47. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/client/compute_validation.py +0 -0
  48. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/client/exceptions.py +0 -0
  49. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/client/github_issues.py +0 -0
  50. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/client/inference.py +0 -0
  51. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/client/models_foundry.py +0 -0
  52. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/client/models_helpers.py +0 -0
  53. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/client/observability.py +0 -0
  54. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/client/utils.py +0 -0
  55. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/client/value_sets.py +0 -0
  56. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/code_repository_context.py +0 -0
  57. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/code_repository_skills.py +0 -0
  58. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/defaults.py +0 -0
  59. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/instrumentation/__init__.py +0 -0
  60. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/instrumentation/utils.py +0 -0
  61. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/logconf.py +0 -0
  62. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/repository_identity_security.py +0 -0
  63. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/runtime_credential_exchange.py +0 -0
  64. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/runtime_flags.py +0 -0
  65. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/scaffold_skills.py +0 -0
  66. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence/server/__init__.py +0 -0
  67. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence.egg-info/SOURCES.txt +0 -0
  68. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence.egg-info/dependency_links.txt +0 -0
  69. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence.egg-info/entry_points.txt +0 -0
  70. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence.egg-info/requires.txt +0 -0
  71. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/mainsequence.egg-info/top_level.txt +0 -0
  72. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/setup.cfg +0 -0
  73. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_auth_bootstrap.py +0 -0
  74. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_auth_precedence.py +0 -0
  75. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_canonical_api_route_contract.py +0 -0
  76. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_cli_auth_contract.py +0 -0
  77. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_cli_browser_auth.py +0 -0
  78. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_cli_secure_storage.py +0 -0
  79. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_client.py +0 -0
  80. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_code_repository_branch_contract.py +0 -0
  81. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_code_repository_documentation_contract.py +0 -0
  82. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_code_repository_runtime_context.py +0 -0
  83. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_code_repository_skills.py +0 -0
  84. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_credential_repr.py +0 -0
  85. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_dependency_extras.py +0 -0
  86. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_dev_release_version.py +0 -0
  87. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_environment_resource_context.py +0 -0
  88. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_environment_response_contracts.py +0 -0
  89. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_github_issues_client.py +0 -0
  90. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_http_retry_budget.py +0 -0
  91. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_independent_context.py +0 -0
  92. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_inference_client.py +0 -0
  93. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_instrumentation.py +0 -0
  94. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_job_client.py +0 -0
  95. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_local_ops.py +0 -0
  96. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_logconf.py +0 -0
  97. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_notification_uid_contract.py +0 -0
  98. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_observability_client.py +0 -0
  99. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_runtime_credential_exchange.py +0 -0
  100. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_scaffold_skills.py +0 -0
  101. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_secret_client_model.py +0 -0
  102. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_source_ownership.py +0 -0
  103. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_ssh_utils.py +0 -0
  104. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_tolerant_response_reading.py +0 -0
  105. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/tests/test_tolerant_value_sets.py +0 -0
  106. {mainsequence-9.0.17.dev60 → mainsequence-9.0.18.dev61}/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.17.dev60
3
+ Version: 9.0.18.dev61
4
4
  Summary: Main Sequence SDK
5
5
  Author-email: Main Sequence GmbH <dev@main-sequence.io>
6
6
  License-Expression: MIT
@@ -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. `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:
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 not in requester.team_uids:
188
- raise HTTPException(status_code=403, detail="The requester is not in this team.")
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 against the requester. The acting
193
- application has no rights of its own unless the API's policy grants them.
194
- - The requester can authorize reads and writes within that person's own
195
- member-level permissions. Before a write, check the exact object and action;
196
- the example's team-membership check is not a general edit, run, share or
197
- delete grant. The getter verifies identity, not operation permission.
198
- - Fail closed: an operation that needs a person refuses the call when
199
- `User.get_requester()` is `None`. Never fall back to the caller's rights.
200
- - Never read a person's UID from the request body, a header or a query
201
- parameter, and never let a caller name whom it acts for. Anyone can write a
202
- UID there. Only the signed assertion names the requester, and the SDK has
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
- - Hosted Agents never receive Secret values. The platform bounds work to at
212
- most 24 hours after the original request and checks access on every call;
213
- delegation does not restart that limit. This SDK does not enable platform
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, and requester-required operations refused
221
- without a requester even when the calling Agent has its own grants.
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. For a tool invoked by an Agent on a person's
237
- behalf, check that requester for both reads and writes. Refuse a
238
- requester-required write when the getter returns `None`, and verify object- and
239
- action-specific permission before mutation; read access and MCP annotations are not
240
- write authority. Ordinary non-requester calls retain their own caller policy.
241
- See
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
- - Reads and the creates, updates, runs, sharing changes and deletes the person
244
- may make, where the platform endpoint supports requester-bound access.
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
- - The requester's member level, never an admin's, even when the requester is
248
- an Organization admin.
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
- These writes and delegation require platform support; installing this SDK does
261
- not enable them. Treat a refused operation as refused, never retry it with the
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, and never lets a
268
- caller choose whom it acts for. A FastAPI application that receives a
269
- requester-bound call reads the requester with `User.get_requester()` and
270
- authorizes reads and writes against that person's object- and operation-specific
271
- permissions:
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` uses the exact
277
- platform-approved statement from
278
- [Platform ADR-0051, What people are told](https://github.com/Main-Sequence-Server-Side/tdag-django/blob/development/docs/platform/adr/adr-0051-requester-bound-access-for-workloads.md#11-what-people-are-told).
279
- Do not invent or paraphrase a separate SDK statement. For write-enabled
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
- The platform behavior and approved write-enabled statement must ship together.
287
- If the platform still supplies only the earlier read-only text, do not claim
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
- member-level permissions allow, including sharing and deleting where
293
- supported, for at most 24 hours after the original request. Only Organization
294
- admins decide which Agents may receive this authority; the application's
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 only by a read grant or the Agent's grants
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 explain both the write risk and the original 24-hour limit
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; its ``is_organization_admin`` is always ``False``.
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 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."
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 the caller as an admin of the application's "
68
- "Organization. False when the request carries no such facts, and always "
69
- "False for the requester of a requester-bound request."
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 requester of a requester-bound request, or ``None``.
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 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.
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 is not requester-bound, including
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
- Authorize reads and writes against the requester's permission for the
1325
- exact object and operation. This getter verifies identity, not
1326
- authorization; read access or team membership does not authorize a
1327
- write. Give the acting application no rights of its own unless your
1328
- policy grants them.
1329
- An operation that needs a person fails closed when this is ``None``.
1330
- Never take a person's UID from a request body, header or query
1331
- parameter instead. Platform-supported requester-bound writes stay
1332
- within the person's member-level rights, never admin powers or Secret
1333
- values. The platform checks access on every call and limits the work to
1334
- at most 24 hours after the original request, including across approved
1335
- Agent delegation. A model-driven Agent can be steered by prompt
1336
- injection, so per-operation checks remain necessary. This getter does
1337
- not enable platform write endpoints or extend its request scope.
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):
@@ -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
- # What the platform states about the caller: active team UIDs and the
38
- # Organization-admin flag. Sent together, or not at all by a platform that
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 = False
77
- # The requester of a requester-bound call, None on any other call. 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
- facts = set(payload) & CALLER_FACT_CLAIMS
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
- claims = set(payload) - facts - {"requester"}
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
- if not facts:
98
- return (), False
99
- team_uids = _team_uids(payload["team_uids"], owner="Caller")
100
- if type(payload["is_organization_admin"]) is not bool:
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 requester of a requester-bound call: exactly ``sub`` and ``team_uids``.
112
+ """The person a requester-bound call works for, or None on any other call.
107
113
 
108
- It never carries an admin flag: the requester's access reaches the
109
- application at member level only.
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) != {"sub", "team_uids"}:
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
- # Never an admin: the requester's access is member level.
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()``. Only authenticated POSTs reach the
401
- server; other methods answer 405 and an ``Origin`` other than the release's
402
- own or its CORS origins answers 403. See ADR 0038.
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 9.0.17.dev60
3
+ Version: 9.0.18.dev61
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.17.dev60"
7
+ version = "9.0.18.dev61"
8
8
  description = "Main Sequence SDK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"