ms-tau-sdk 2.0.0.dev29__tar.gz → 2.0.0.dev32__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 (82) hide show
  1. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/PKG-INFO +1 -1
  2. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/pyproject.toml +1 -1
  3. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/api/models.py +3 -0
  4. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/api/sessions.py +10 -1
  5. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/backend/client.py +53 -23
  6. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/backend/local.py +110 -11
  7. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/backend/models.py +7 -0
  8. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/providers/factory.py +14 -0
  9. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/runtime/manager.py +7 -0
  10. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/settings.py +4 -0
  11. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/.gitignore +0 -0
  12. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/CHANGELOG.md +0 -0
  13. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/README.md +0 -0
  14. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/packages/tau-board/src/ms_tau_board/__init__.py +0 -0
  15. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/packages/tau-board/src/ms_tau_board/app.py +0 -0
  16. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/packages/tau-board/src/ms_tau_board/cli.py +0 -0
  17. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/packages/tau-board/src/ms_tau_board/config.py +0 -0
  18. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/packages/tau-board/src/ms_tau_board/env_file.py +0 -0
  19. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/packages/tau-board/src/ms_tau_board/logs.py +0 -0
  20. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/packages/tau-board/src/ms_tau_board/proxy.py +0 -0
  21. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/packages/tau-board/src/ms_tau_board/state.py +0 -0
  22. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/packages/tau-board/src/ms_tau_board/static/BULMA-LICENSE.txt +0 -0
  23. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/packages/tau-board/src/ms_tau_board/static/app.js +0 -0
  24. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/packages/tau-board/src/ms_tau_board/static/board.css +0 -0
  25. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/packages/tau-board/src/ms_tau_board/static/bulma.min.css +0 -0
  26. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/packages/tau-board/src/ms_tau_board/static/index.html +0 -0
  27. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/__init__.py +0 -0
  28. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/agent_skills/tau_a2a_runtime_adapter/SKILL.md +0 -0
  29. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/agent_skills/tau_local_development/SKILL.md +0 -0
  30. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/agent_skills/tau_project_customization/SKILL.md +0 -0
  31. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/agent_skills/tau_repository_integration/SKILL.md +0 -0
  32. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/api/__init__.py +0 -0
  33. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/api/a2a.py +0 -0
  34. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/api/chat.py +0 -0
  35. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/api/conversations.py +0 -0
  36. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/api/dependencies.py +0 -0
  37. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/api/health.py +0 -0
  38. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/api/inspection.py +0 -0
  39. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/api/local_chat.py +0 -0
  40. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/api/request_identity.py +0 -0
  41. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/app.py +0 -0
  42. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/application.py +0 -0
  43. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/backend/__init__.py +0 -0
  44. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/backend/assertions.py +0 -0
  45. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/backend/auth.py +0 -0
  46. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/backend/mcp.py +0 -0
  47. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/backend/routes.py +0 -0
  48. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/cli.py +0 -0
  49. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/errors.py +0 -0
  50. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/logging.py +0 -0
  51. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/protocols/__init__.py +0 -0
  52. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/protocols/a2a_failure.py +0 -0
  53. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/protocols/a2a_message.py +0 -0
  54. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/protocols/a2a_roles.py +0 -0
  55. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/protocols/assistant_ui.py +0 -0
  56. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/protocols/chat_history.py +0 -0
  57. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/protocols/strict_json.py +0 -0
  58. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/providers/__init__.py +0 -0
  59. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/providers/definitions.py +0 -0
  60. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/providers/tau_compat.py +0 -0
  61. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/resources/SYSTEM.md +0 -0
  62. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/resources/__init__.py +0 -0
  63. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/resources/loader.py +0 -0
  64. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/resources/prompts/review-code-repository.md +0 -0
  65. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/runtime/__init__.py +0 -0
  66. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/runtime/deployment_health.py +0 -0
  67. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/runtime/events.py +0 -0
  68. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/runtime/extensions.py +0 -0
  69. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/runtime/failures.py +0 -0
  70. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/runtime/live_turns.py +0 -0
  71. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/runtime/observability.py +0 -0
  72. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/runtime/provenance.py +0 -0
  73. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/runtime/session.py +0 -0
  74. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/runtime/snapshots.py +0 -0
  75. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/runtime/task_context.py +0 -0
  76. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/sessions/__init__.py +0 -0
  77. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/sessions/storage.py +0 -0
  78. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/skills.py +0 -0
  79. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/tools/__init__.py +0 -0
  80. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/tools/mainsequence_mcp.py +0 -0
  81. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/tools/skill_read.py +0 -0
  82. {ms_tau_sdk-2.0.0.dev29 → ms_tau_sdk-2.0.0.dev32}/src/ms_tau_sdk/tools/task_control.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ms-tau-sdk
3
- Version: 2.0.0.dev29
3
+ Version: 2.0.0.dev32
4
4
  Summary: Workspace-bound Tau application primitives for Main Sequence projects
5
5
  Project-URL: Changelog, https://github.com/mainsequence-sdk/ms-tau-sdk/blob/development/CHANGELOG.md
6
6
  Project-URL: Documentation, https://github.com/mainsequence-sdk/ms-tau-sdk/tree/development/docs
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ms-tau-sdk"
7
- version = "2.0.0.dev29"
7
+ version = "2.0.0.dev32"
8
8
  description = "Workspace-bound Tau application primitives for Main Sequence projects"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -57,6 +57,9 @@ class CancelRequest(ApiModel):
57
57
 
58
58
 
59
59
  class SessionModelSelection(ApiModel):
60
+ custom_id: str | None = Field(
61
+ default=None, validation_alias=AliasChoices("customId", "custom_id")
62
+ )
60
63
  session_uid: str = Field(validation_alias=AliasChoices("sessionUid", "session_uid"))
61
64
  provider: str
62
65
  model: str
@@ -37,6 +37,7 @@ async def session_model(
37
37
  "provider": session.active_provider,
38
38
  "model": session.active_model,
39
39
  "thinkingLevel": session.active_thinking,
40
+ **({"customId": session.custom_id} if session.custom_id is not None else {}),
40
41
  },
41
42
  }
42
43
 
@@ -45,7 +46,13 @@ async def session_model(
45
46
  async def model_providers(client: BackendDep, config: SettingsDep) -> dict[str, object]:
46
47
  if not config.local_mode:
47
48
  raise HTTPException(status_code=409, detail="Provider catalog is local-mode only")
48
- return await client.list_model_providers()
49
+ return await client.list_model_providers(
50
+ **(
51
+ {"organization_environment_uid": config.local_organization_environment_uid}
52
+ if config.local_organization_environment_uid
53
+ else {}
54
+ )
55
+ )
49
56
 
50
57
 
51
58
  @router.put("/session-model")
@@ -62,6 +69,7 @@ async def select_session_model(
62
69
  provider=body.provider,
63
70
  model=body.model,
64
71
  thinking_level=body.thinking_level,
72
+ **({"custom_id": body.custom_id} if body.custom_id is not None else {}),
65
73
  )
66
74
  session = await manager.backend.get_session(session_uid)
67
75
  return {
@@ -70,6 +78,7 @@ async def select_session_model(
70
78
  "provider": session.active_provider,
71
79
  "model": session.active_model,
72
80
  "thinkingLevel": session.active_thinking,
81
+ **({"customId": session.custom_id} if session.custom_id is not None else {}),
73
82
  },
74
83
  }
75
84
 
@@ -356,9 +356,16 @@ class MainSequenceClient:
356
356
  detail=error.errors(include_input=False),
357
357
  ) from error
358
358
 
359
- async def list_model_providers(self) -> dict[str, Any]:
359
+ async def list_model_providers(
360
+ self, *, organization_environment_uid: str | None = None
361
+ ) -> dict[str, Any]:
360
362
  """Read the authenticated user's safe provider catalog from Main Sequence."""
361
- data = await self._request("GET", MODEL_PROVIDERS, idempotent=True)
363
+ options: dict[str, Any] = (
364
+ {"params": {"organization_environment_uid": organization_environment_uid}}
365
+ if organization_environment_uid
366
+ else {}
367
+ )
368
+ data = await self._request("GET", MODEL_PROVIDERS, idempotent=True, **options)
362
369
  if not isinstance(data, dict) or not isinstance(data.get("providers"), list):
363
370
  raise BackendError("Backend model-provider catalog response is invalid")
364
371
  return data
@@ -370,6 +377,7 @@ class MainSequenceClient:
370
377
  provider: str,
371
378
  model: str,
372
379
  thinking_level: str | None,
380
+ custom_id: str | None = None,
373
381
  ) -> AgentSession:
374
382
  payload: dict[str, Any] = {
375
383
  "llm_provider": provider,
@@ -377,13 +385,18 @@ class MainSequenceClient:
377
385
  }
378
386
  if thinking_level is not None:
379
387
  payload["llm_thinking"] = thinking_level
388
+ if custom_id is not None:
389
+ payload["custom_id"] = custom_id
380
390
  data = await self._request(
381
391
  "PATCH",
382
392
  agent_session(session_uid),
383
393
  json=payload,
384
394
  idempotent=True,
385
395
  )
386
- return AgentSession.model_validate(data)
396
+ session = AgentSession.model_validate(data)
397
+ if custom_id is not None and session.custom_id != custom_id:
398
+ raise BackendError("Backend did not select the requested configured provider")
399
+ return session
387
400
 
388
401
  async def get_agent_card(self, session_uid: str) -> AgentCardEnvelope:
389
402
  data = await self._request(
@@ -601,23 +614,27 @@ class MainSequenceClient:
601
614
  model: str,
602
615
  session_uid: str,
603
616
  holder_id: str,
617
+ custom_id: str | None = None,
604
618
  ) -> ProviderExecutionEvidence:
619
+ payload: dict[str, Any] = {
620
+ "agent_session_uid": session_uid,
621
+ "providers": [provider],
622
+ "holder_id": holder_id,
623
+ "supported_provider_control_schema_versions": [1],
624
+ "execution_selection": {"provider": provider, "model": model},
625
+ }
626
+ if custom_id is not None:
627
+ payload["custom_id"] = custom_id
605
628
  data = await self._request(
606
629
  "POST",
607
630
  model_provider_credentials("hydrate"),
608
- json={
609
- "agent_session_uid": session_uid,
610
- "providers": [provider],
611
- "holder_id": holder_id,
612
- "supported_provider_control_schema_versions": [1],
613
- "execution_selection": {
614
- "provider": provider,
615
- "model": model,
616
- },
617
- },
631
+ json=payload,
618
632
  idempotent=True,
619
633
  )
620
- return self._provider_evidence(provider, data)
634
+ evidence = self._provider_evidence(provider, data)
635
+ if custom_id is not None and evidence.credential.custom_id != custom_id:
636
+ raise BackendError("Backend did not hydrate the requested configured provider")
637
+ return evidence
621
638
 
622
639
  async def hydrate_local_provider_credential(
623
640
  self,
@@ -626,24 +643,33 @@ class MainSequenceClient:
626
643
  model: str,
627
644
  thinking_level: str | None,
628
645
  holder_id: str,
646
+ custom_id: str | None = None,
647
+ organization_environment_uid: str | None = None,
629
648
  ) -> ProviderExecutionEvidence:
630
- """Hydrate one explicit selection for the authenticated local user."""
631
-
649
+ """Hydrate an owned or shared selection using the backend's access checks."""
632
650
  selection: dict[str, str] = {"provider": provider, "model": model}
633
651
  if thinking_level:
634
652
  selection["thinking_level"] = thinking_level
653
+ payload: dict[str, Any] = {
654
+ "providers": [provider],
655
+ "holder_id": holder_id,
656
+ "supported_provider_control_schema_versions": [1],
657
+ "execution_selection": selection,
658
+ }
659
+ if organization_environment_uid is not None:
660
+ payload["organization_environment_uid"] = organization_environment_uid
661
+ if custom_id is not None:
662
+ payload["custom_id"] = custom_id
635
663
  data = await self._request(
636
664
  "POST",
637
665
  model_provider_credentials("hydrate"),
638
- json={
639
- "providers": [provider],
640
- "holder_id": holder_id,
641
- "supported_provider_control_schema_versions": [1],
642
- "execution_selection": selection,
643
- },
666
+ json=payload,
644
667
  idempotent=True,
645
668
  )
646
- return self._provider_evidence(provider, data)
669
+ evidence = self._provider_evidence(provider, data)
670
+ if custom_id is not None and evidence.credential.custom_id != custom_id:
671
+ raise BackendError("Backend did not hydrate the requested configured provider")
672
+ return evidence
647
673
 
648
674
  def _provider_evidence(self, provider: str, data: object) -> ProviderExecutionEvidence:
649
675
  if not isinstance(data, dict):
@@ -679,6 +705,10 @@ class MainSequenceClient:
679
705
  return ProviderCredential.model_validate(
680
706
  {
681
707
  "provider": provider,
708
+ "custom_id": hydrated.get("custom_id"),
709
+ "model_provider_credential_uid": hydrated.get("uid"),
710
+ "organization_environment_uid": hydrated.get("organization_environment_uid"),
711
+ "owner_user_uid": hydrated.get("user_uid"),
682
712
  "credential_kind": credential_kind,
683
713
  "api_key": raw.get("api_key") or raw.get("key"),
684
714
  "access_token": access_token,
@@ -147,8 +147,15 @@ class LocalDevelopmentBackend(MainSequenceClient):
147
147
  async def aclose(self) -> None:
148
148
  await self._services.aclose()
149
149
 
150
- async def list_model_providers(self) -> dict[str, Any]:
151
- return await self._services.list_model_providers()
150
+ async def list_model_providers(
151
+ self, *, organization_environment_uid: str | None = None
152
+ ) -> dict[str, Any]:
153
+ environment_uid = (
154
+ organization_environment_uid or self.settings.local_organization_environment_uid
155
+ )
156
+ return await self._services.list_model_providers(
157
+ **({"organization_environment_uid": environment_uid} if environment_uid else {})
158
+ )
152
159
 
153
160
  async def _ensure_initialized(self) -> None:
154
161
  if self._initialized:
@@ -391,6 +398,13 @@ class LocalDevelopmentBackend(MainSequenceClient):
391
398
  "TEXT NOT NULL DEFAULT ''",
392
399
  )
393
400
  self._ensure_schema_column(connection, "sessions", "active_task_attempt_uid", "TEXT")
401
+ self._ensure_schema_column(connection, "sessions", "custom_id", "TEXT")
402
+ self._ensure_schema_column(
403
+ connection, "sessions", "model_provider_credential_uid", "TEXT"
404
+ )
405
+ self._ensure_schema_column(
406
+ connection, "sessions", "organization_environment_uid", "TEXT"
407
+ )
394
408
  self._ensure_schema_column(connection, "entries", "turn_uid", "TEXT")
395
409
  self._ensure_schema_column(connection, "a2a_task_attempts", "turn_uid", "TEXT")
396
410
  self._ensure_schema_column(
@@ -513,14 +527,31 @@ class LocalDevelopmentBackend(MainSequenceClient):
513
527
  return "sha256:" + hashlib.sha256(payload).hexdigest()
514
528
 
515
529
  async def _hydrate_evidence(
516
- self, *, holder_id: str, selection: tuple[str, str, str | None] | None = None
530
+ self,
531
+ *,
532
+ holder_id: str,
533
+ selection: tuple[str, str, str | None] | None = None,
534
+ custom_id: str | None = None,
535
+ credential_uid: str | None = None,
536
+ organization_environment_uid: str | None = None,
517
537
  ) -> ProviderExecutionEvidence:
518
538
  provider, model, thinking = selection or self._selection()
539
+ options: dict[str, Any] = {}
540
+ if custom_id is None and selection is None:
541
+ custom_id = self.settings.local_custom_id
542
+ environment_uid = (
543
+ organization_environment_uid or self.settings.local_organization_environment_uid
544
+ )
545
+ if custom_id is not None:
546
+ options["custom_id"] = custom_id
547
+ if environment_uid is not None:
548
+ options["organization_environment_uid"] = environment_uid
519
549
  evidence = await self._services.hydrate_local_provider_credential(
520
550
  provider,
521
551
  model=model,
522
552
  thinking_level=thinking,
523
553
  holder_id=holder_id,
554
+ **options,
524
555
  )
525
556
  if (
526
557
  evidence.provider_control.provider != provider
@@ -529,6 +560,16 @@ class LocalDevelopmentBackend(MainSequenceClient):
529
560
  raise BackendConflictError(
530
561
  "Main Sequence provider evidence does not match the local selection"
531
562
  )
563
+ if (
564
+ credential_uid is not None
565
+ and evidence.credential.model_provider_credential_uid != credential_uid
566
+ ):
567
+ raise BackendConflictError("Main Sequence changed the local session credential")
568
+ if (
569
+ environment_uid is not None
570
+ and evidence.credential.organization_environment_uid != environment_uid
571
+ ):
572
+ raise BackendConflictError("Main Sequence changed the local session Environment")
532
573
  return evidence
533
574
 
534
575
  async def prefetch_provider(self) -> None:
@@ -560,6 +601,10 @@ class LocalDevelopmentBackend(MainSequenceClient):
560
601
  return {
561
602
  "credentials": {
562
603
  credential.provider: {
604
+ "uid": credential.model_provider_credential_uid,
605
+ "custom_id": credential.custom_id,
606
+ "organization_environment_uid": credential.organization_environment_uid,
607
+ "user_uid": credential.owner_user_uid,
563
608
  "credential_kind": credential.credential_kind,
564
609
  "credential": {key: value for key, value in raw.items() if value is not None},
565
610
  "version": credential.metadata.get("version"),
@@ -576,14 +621,24 @@ class LocalDevelopmentBackend(MainSequenceClient):
576
621
  session_uid: str | None = None,
577
622
  agent_uid: str | None = None,
578
623
  holder_id: str,
624
+ custom_id: str | None = None,
579
625
  ) -> ProviderExecutionEvidence:
580
626
  del agent_uid
581
627
  selected_provider, selected_model, selected_thinking = self._selection()
628
+ credential_uid = None
629
+ environment_uid = None
582
630
  if session_uid is not None:
583
631
  session = await self.get_session(session_uid)
584
632
  selected_provider = session.active_provider or ""
585
633
  selected_model = session.active_model or ""
586
634
  selected_thinking = session.active_thinking
635
+ if custom_id is not None and custom_id != session.custom_id:
636
+ raise BackendConflictError(
637
+ "Local refresh must retain the session's configured provider"
638
+ )
639
+ custom_id = session.custom_id
640
+ credential_uid = session.model_provider_credential_uid
641
+ environment_uid = session.organization_environment_uid
587
642
  if provider != selected_provider or model != selected_model:
588
643
  raise BackendConflictError(
589
644
  "Local provider refresh does not match the session selection"
@@ -595,6 +650,9 @@ class LocalDevelopmentBackend(MainSequenceClient):
595
650
  return await self._hydrate_evidence(
596
651
  holder_id=holder_id,
597
652
  selection=(selected_provider, selected_model, selected_thinking),
653
+ custom_id=custom_id,
654
+ credential_uid=credential_uid,
655
+ organization_environment_uid=environment_uid,
598
656
  )
599
657
 
600
658
  async def get_session(self, session_uid: str) -> AgentSession:
@@ -617,13 +675,19 @@ class LocalDevelopmentBackend(MainSequenceClient):
617
675
  provider: str,
618
676
  model: str,
619
677
  thinking_level: str | None,
678
+ custom_id: str | None = None,
620
679
  ) -> AgentSession:
621
680
  if not provider or not model:
622
681
  raise BackendConflictError("A provider and model are required")
623
682
  selection = (provider, model, thinking_level)
624
- await self._hydrate_evidence(
683
+ previous = await self.get_session(session_uid)
684
+ retained = custom_id is None and previous.active_provider == provider
685
+ evidence = await self._hydrate_evidence(
625
686
  holder_id=f"local-selection-{self.settings.workspace_digest}",
626
687
  selection=selection,
688
+ custom_id=previous.custom_id if retained else custom_id,
689
+ credential_uid=previous.model_provider_credential_uid if retained else None,
690
+ organization_environment_uid=previous.organization_environment_uid,
627
691
  )
628
692
 
629
693
  def operation() -> None:
@@ -637,11 +701,16 @@ class LocalDevelopmentBackend(MainSequenceClient):
637
701
  raise BackendConflictError("Cannot change a model while the session is working")
638
702
  connection.execute(
639
703
  "UPDATE sessions SET provider = ?, model = ?, thinking = ?, "
640
- "runtime_config_sha256 = ?, updated_at = ? WHERE uid = ?",
704
+ "runtime_config_sha256 = ?, updated_at = ?, custom_id = ?, "
705
+ "model_provider_credential_uid = ?, "
706
+ "organization_environment_uid = ? WHERE uid = ?",
641
707
  (
642
708
  *selection,
643
709
  self._runtime_config_sha256(selection),
644
710
  _iso(_utcnow()),
711
+ evidence.credential.custom_id,
712
+ evidence.credential.model_provider_credential_uid,
713
+ evidence.credential.organization_environment_uid,
645
714
  session_uid,
646
715
  ),
647
716
  )
@@ -1504,6 +1573,9 @@ class LocalDevelopmentBackend(MainSequenceClient):
1504
1573
  "active_provider": str(row["provider"]),
1505
1574
  "active_model": str(row["model"]),
1506
1575
  "active_thinking": row["thinking"],
1576
+ "custom_id": row["custom_id"],
1577
+ "model_provider_credential_uid": row["model_provider_credential_uid"],
1578
+ "organization_environment_uid": row["organization_environment_uid"],
1507
1579
  "status": "local",
1508
1580
  "runtime_capabilities": LOCAL_RUNTIME_CAPABILITIES,
1509
1581
  "runtime_config_sha256": str(row["runtime_config_sha256"]),
@@ -1586,18 +1658,31 @@ class LocalDevelopmentBackend(MainSequenceClient):
1586
1658
  session_uid: str,
1587
1659
  request: TauRuntimeBootstrapRequest,
1588
1660
  ) -> TauRuntimeBootstrap:
1589
- def stored_selection() -> tuple[str, str, str | None]:
1661
+ def stored_selection() -> tuple[
1662
+ tuple[str, str, str | None], str | None, str | None, str | None
1663
+ ]:
1590
1664
  with closing(self._connect()) as connection:
1591
1665
  row = connection.execute(
1592
- "SELECT provider, model, thinking FROM sessions WHERE uid = ?", (session_uid,)
1666
+ "SELECT * FROM sessions WHERE uid = ?", (session_uid,)
1593
1667
  ).fetchone()
1594
1668
  if row is None:
1595
- return self._selection()
1596
- return str(row["provider"]), str(row["model"]), row["thinking"]
1669
+ return self._selection(), self.settings.local_custom_id, None, None
1670
+ return (
1671
+ (str(row["provider"]), str(row["model"]), row["thinking"]),
1672
+ row["custom_id"],
1673
+ row["model_provider_credential_uid"],
1674
+ row["organization_environment_uid"],
1675
+ )
1597
1676
 
1598
- selection = await self._run(stored_selection)
1677
+ selection, custom_id, credential_uid, environment_uid = await self._run(stored_selection)
1599
1678
  provider, model, thinking = selection
1600
- evidence = await self._hydrate_evidence(holder_id=request.holder_id, selection=selection)
1679
+ evidence = await self._hydrate_evidence(
1680
+ holder_id=request.holder_id,
1681
+ selection=selection,
1682
+ custom_id=custom_id,
1683
+ credential_uid=credential_uid,
1684
+ organization_environment_uid=environment_uid,
1685
+ )
1601
1686
 
1602
1687
  def operation() -> TauRuntimeBootstrap:
1603
1688
  now = _utcnow()
@@ -1640,6 +1725,20 @@ class LocalDevelopmentBackend(MainSequenceClient):
1640
1725
  raise BackendConflictError(
1641
1726
  "Existing local session uses a different provider/model selection"
1642
1727
  )
1728
+ if row is not None and row["model_provider_credential_uid"] is None:
1729
+ connection.execute(
1730
+ "UPDATE sessions SET custom_id = ?, model_provider_credential_uid = ?, "
1731
+ "organization_environment_uid = ? WHERE uid = ?",
1732
+ (
1733
+ evidence.credential.custom_id,
1734
+ evidence.credential.model_provider_credential_uid,
1735
+ evidence.credential.organization_environment_uid,
1736
+ session_uid,
1737
+ ),
1738
+ )
1739
+ row = connection.execute(
1740
+ "SELECT * FROM sessions WHERE uid = ?", (session_uid,)
1741
+ ).fetchone()
1643
1742
  assert row is not None
1644
1743
  current_lease = connection.execute(
1645
1744
  "SELECT * FROM leases WHERE session_uid = ?",
@@ -52,7 +52,10 @@ class BackendRequestModel(BackendModel):
52
52
 
53
53
 
54
54
  class AgentSession(BackendModel):
55
+ organization_environment_uid: str | None = None
55
56
  uid: str
57
+ custom_id: str | None = None
58
+ model_provider_credential_uid: str | None = None
56
59
  agent_uid: str | None = None
57
60
  harness: HarnessKind
58
61
  harness_protocol: HarnessProtocol
@@ -389,6 +392,10 @@ class TauResumeSnapshotUploadResponse(BackendModel):
389
392
 
390
393
  class ProviderCredential(BackendModel):
391
394
  provider: str
395
+ custom_id: str | None = None
396
+ model_provider_credential_uid: str | None = None
397
+ organization_environment_uid: str | None = None
398
+ owner_user_uid: str | None = None
392
399
  credential_kind: str = "api_key"
393
400
  api: str | None = None
394
401
  api_key: SecretStr | None = None
@@ -291,6 +291,11 @@ class ProviderFactory:
291
291
  credential = evidence.credential
292
292
  if credential.provider != provider_name:
293
293
  raise ConfigurationError("Backend credential does not match the selected provider")
294
+ if (
295
+ session.model_provider_credential_uid is not None
296
+ and credential.model_provider_credential_uid != session.model_provider_credential_uid
297
+ ):
298
+ raise ConfigurationError("Backend credential does not match the session binding")
294
299
  thinking_level = self.validate_execution(
295
300
  evidence.provider_control,
296
301
  provider_name=provider_name,
@@ -324,6 +329,15 @@ class ProviderFactory:
324
329
  "Provider-control execution capability changed during "
325
330
  "credential refresh"
326
331
  )
332
+ if cached_credential.model_provider_credential_uid is not None and (
333
+ refreshed.credential.model_provider_credential_uid
334
+ != cached_credential.model_provider_credential_uid
335
+ or refreshed.credential.organization_environment_uid
336
+ != cached_credential.organization_environment_uid
337
+ ):
338
+ raise ConfigurationError(
339
+ "Credential refresh changed the configured provider"
340
+ )
327
341
  cached_credential = refreshed.credential
328
342
  return cached_credential
329
343
 
@@ -2080,6 +2080,7 @@ class SessionRuntimeManager:
2080
2080
  provider: str,
2081
2081
  model: str,
2082
2082
  thinking_level: str | None,
2083
+ custom_id: str | None = None,
2083
2084
  ) -> None:
2084
2085
  """Switch an idle session and reload it with backend-authorized evidence."""
2085
2086
  if not self.settings.local_mode:
@@ -2105,6 +2106,7 @@ class SessionRuntimeManager:
2105
2106
  provider=provider,
2106
2107
  model=model,
2107
2108
  thinking_level=thinking_level,
2109
+ **({"custom_id": custom_id} if custom_id is not None else {}),
2108
2110
  )
2109
2111
  except BaseException:
2110
2112
  runtime.evicting = False
@@ -2119,6 +2121,11 @@ class SessionRuntimeManager:
2119
2121
  provider=previous.active_provider,
2120
2122
  model=previous.active_model,
2121
2123
  thinking_level=previous.active_thinking,
2124
+ **(
2125
+ {"custom_id": previous.custom_id}
2126
+ if previous.custom_id is not None
2127
+ else {}
2128
+ ),
2122
2129
  )
2123
2130
  await self._load(session_uid)
2124
2131
  except Exception:
@@ -152,6 +152,10 @@ class TauSDKSettings(BaseSettings):
152
152
  default=False,
153
153
  validation_alias="TAU_EXCLUDE_MAINSEQUENCE_MCP",
154
154
  )
155
+ local_custom_id: str | None = Field(default=None, validation_alias="TAU_LOCAL_CUSTOM_ID")
156
+ local_organization_environment_uid: str | None = Field(
157
+ default=None, validation_alias="TAU_LOCAL_ORGANIZATION_ENVIRONMENT_UID"
158
+ )
155
159
  local_provider: str | None = Field(default=None, validation_alias="TAU_LOCAL_PROVIDER")
156
160
  local_model: str | None = Field(default=None, validation_alias="TAU_LOCAL_MODEL")
157
161
  local_thinking: (