fred-runtime 3.1.0__tar.gz → 3.1.1__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 (111) hide show
  1. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/PKG-INFO +1 -1
  2. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/integrations/v2_runtime/adapters.py +78 -7
  3. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime.egg-info/PKG-INFO +1 -1
  4. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/pyproject.toml +1 -1
  5. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/tests/test_fred_workspace_fs.py +98 -14
  6. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/tests/test_kf_workspace_client.py +3 -1
  7. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/README.md +0 -0
  8. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/__init__.py +0 -0
  9. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/app/__init__.py +0 -0
  10. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/app/_catalogs.py +0 -0
  11. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/app/agent_app.py +0 -0
  12. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/app/config.py +0 -0
  13. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/app/config_loader.py +0 -0
  14. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/app/container.py +0 -0
  15. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/app/context.py +0 -0
  16. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/app/dependencies.py +0 -0
  17. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/app/mcp_config.py +0 -0
  18. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/app/observability_factory.py +0 -0
  19. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/app/openai_compat_router.py +0 -0
  20. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/cli/__init__.py +0 -0
  21. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/cli/completion.py +0 -0
  22. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/cli/entrypoint.py +0 -0
  23. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/cli/history_display.py +0 -0
  24. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/cli/kpi_display.py +0 -0
  25. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/cli/pod_client.py +0 -0
  26. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/cli/repl.py +0 -0
  27. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/cli/repl_helpers.py +0 -0
  28. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/cli/url_helpers.py +0 -0
  29. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/client.py +0 -0
  30. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/common/__init__.py +0 -0
  31. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/common/context_aware_tool.py +0 -0
  32. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/common/kf_base_client.py +0 -0
  33. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/common/kf_fast_text_client.py +0 -0
  34. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/common/kf_http_client.py +0 -0
  35. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/common/kf_logs_client.py +0 -0
  36. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/common/kf_markdown_media_client.py +0 -0
  37. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/common/kf_vectorsearch_client.py +0 -0
  38. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/common/kf_workspace_client.py +1 -1
  39. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/common/mcp_interceptors.py +0 -0
  40. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/common/mcp_runtime.py +0 -0
  41. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/common/mcp_toolkit.py +0 -0
  42. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/common/mcp_utils.py +0 -0
  43. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/common/structures.py +0 -0
  44. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/common/token_expiry.py +0 -0
  45. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/common/tool_node_utils.py +0 -0
  46. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/deep/__init__.py +0 -0
  47. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/deep/deep_runtime.py +0 -0
  48. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/eval/__init__.py +0 -0
  49. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/eval/collector.py +0 -0
  50. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/graph/__init__.py +0 -0
  51. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/graph/graph_runtime.py +0 -0
  52. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/integrations/__init__.py +0 -0
  53. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/integrations/v2_runtime/__init__.py +0 -0
  54. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/model_routing/__init__.py +0 -0
  55. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/model_routing/catalog.py +0 -0
  56. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/model_routing/contracts.py +0 -0
  57. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/model_routing/provider.py +0 -0
  58. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/model_routing/resolver.py +0 -0
  59. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/react/__init__.py +0 -0
  60. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/react/react_langchain_adapter.py +0 -0
  61. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/react/react_message_codec.py +0 -0
  62. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/react/react_model_adapter.py +0 -0
  63. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/react/react_prompting.py +0 -0
  64. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/react/react_runtime.py +0 -0
  65. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/react/react_stream_adapter.py +0 -0
  66. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/react/react_tool_binding.py +0 -0
  67. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/react/react_tool_loop.py +0 -0
  68. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/react/react_tool_rendering.py +0 -0
  69. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/react/react_tool_resolution.py +0 -0
  70. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/react/react_tool_utils.py +0 -0
  71. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/react/react_tracing.py +0 -0
  72. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/runtime_context.py +0 -0
  73. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/runtime_support/__init__.py +0 -0
  74. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/runtime_support/checkpoints.py +0 -0
  75. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/runtime_support/model_metadata.py +0 -0
  76. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/runtime_support/request_context_helpers.py +0 -0
  77. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/runtime_support/sql_checkpointer.py +0 -0
  78. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/runtime_support/user_token_refresher.py +0 -0
  79. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/support/__init__.py +0 -0
  80. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/support/filesystem_context.py +0 -0
  81. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/support/thinking.py +0 -0
  82. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/support/tool_approval.py +0 -0
  83. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime/support/tool_loop.py +0 -0
  84. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime.egg-info/SOURCES.txt +0 -0
  85. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime.egg-info/dependency_links.txt +0 -0
  86. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime.egg-info/entry_points.txt +0 -0
  87. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime.egg-info/requires.txt +0 -0
  88. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/fred_runtime.egg-info/top_level.txt +0 -0
  89. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/setup.cfg +0 -0
  90. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/tests/test_agent_app.py +0 -0
  91. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/tests/test_client.py +0 -0
  92. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/tests/test_config_loader.py +0 -0
  93. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/tests/test_context.py +0 -0
  94. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/tests/test_context_aware_tool.py +0 -0
  95. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/tests/test_conversational_memory.py +0 -0
  96. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/tests/test_eval_collector.py +0 -0
  97. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/tests/test_eval_trace.py +0 -0
  98. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/tests/test_graph_runtime_invoke_agent.py +0 -0
  99. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/tests/test_graph_runtime_observability.py +0 -0
  100. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/tests/test_history.py +0 -0
  101. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/tests/test_kpi_display.py +0 -0
  102. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/tests/test_mcp_config.py +0 -0
  103. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/tests/test_model_routing.py +0 -0
  104. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/tests/test_openai_compat_router.py +0 -0
  105. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/tests/test_pod_client.py +0 -0
  106. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/tests/test_react_thinking.py +0 -0
  107. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/tests/test_repl_helpers.py +0 -0
  108. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/tests/test_smoke.py +0 -0
  109. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/tests/test_token_expiry.py +0 -0
  110. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/tests/test_url_helpers.py +0 -0
  111. {fred_runtime-3.1.0 → fred_runtime-3.1.1}/tests/test_user_token_refresher.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fred-runtime
3
- Version: 3.1.0
3
+ Version: 3.1.1
4
4
  Summary: Runtime adapters and infrastructure wiring for Fred v2 agents.
5
5
  Author-email: Thales <noreply@thalesgroup.com>
6
6
  License: Apache-2.0
@@ -923,18 +923,35 @@ class FredWorkspaceFs(WorkspaceFsPort):
923
923
  )
924
924
  return str(uid)
925
925
 
926
+ def _session_agent_instance_id(self) -> str:
927
+ # The agents subtree is keyed by the immutable per-team agent_instance_id
928
+ # (FILES-04 / AGENT-FILESYSTEM-RFC §3.1), injected from the execution grant
929
+ # — never the template agent_id, never agent-supplied.
930
+ aid = getattr(self._binding.runtime_context, "agent_instance_id", None)
931
+ if not aid:
932
+ raise RuntimeError(
933
+ "Workspace filesystem requires an agent instance in the session context."
934
+ )
935
+ return str(aid)
936
+
926
937
  def _token(self) -> str:
927
938
  return _workspace_access_token(self._binding.runtime_context)
928
939
 
929
940
  # ---- path relativization (§7.1 security rule) ----
930
941
  def _resolve(self, path: str, *, allow_root: bool = False) -> str:
931
942
  team = self._session_team()
943
+ # Bare agent paths resolve to the running agent's own per-user space
944
+ # (FILES-04 / AGENT-FILESYSTEM-RFC §3, §6), not Mon espace.
945
+ agent_root = (
946
+ f"teams/{team}/agents/{self._session_agent_instance_id()}"
947
+ f"/users/{self._session_user()}"
948
+ )
932
949
  parts = [p for p in (path or "").strip().replace("\\", "/").split("/") if p]
933
950
  if ".." in parts:
934
951
  raise ValueError("Path cannot contain parent path segments")
935
952
  if not parts:
936
953
  if allow_root:
937
- return f"teams/{team}/users/{self._session_user()}"
954
+ return agent_root
938
955
  raise ValueError("Path cannot be empty")
939
956
  head = parts[0]
940
957
  if head == "teams":
@@ -946,24 +963,78 @@ class FredWorkspaceFs(WorkspaceFsPort):
946
963
  )
947
964
  return "/".join(parts)
948
965
  if head == "shared":
966
+ # Team-shared reads (e.g. resolve_template's team step) stay addressable;
967
+ # write/delete into shared is rejected separately (agents never share).
949
968
  return f"teams/{team}/" + "/".join(parts)
950
- return f"teams/{team}/users/{self._session_user()}/" + "/".join(parts)
969
+ return f"{agent_root}/" + "/".join(parts)
970
+
971
+ def _agent_root(self) -> str:
972
+ return (
973
+ f"teams/{self._session_team()}/agents/{self._session_agent_instance_id()}"
974
+ f"/users/{self._session_user()}"
975
+ )
976
+
977
+ def _resolve_owned(self, path: str) -> str:
978
+ """
979
+ Resolve a path the agent must own — used for write and delete.
980
+
981
+ Agents read team-shared files and their own space, but may only *mutate*
982
+ inside their own agents subtree. A path resolving outside it — into
983
+ ``shared/`` (G3: agents never share), Mon espace, or a sibling agent's
984
+ subtree (G2) — is a hard ``PermissionError`` (AGENT-FILESYSTEM-RFC §6).
985
+ """
986
+ resolved = self._resolve(path)
987
+ root = self._agent_root()
988
+ if resolved != root and not resolved.startswith(root + "/"):
989
+ raise PermissionError(
990
+ f"Agents may only write inside their own space; '{path}' resolves outside it."
991
+ )
992
+ return resolved
993
+
994
+ def _clean_parts(self, path: str) -> list[str]:
995
+ parts = [p for p in (path or "").strip().replace("\\", "/").split("/") if p]
996
+ if ".." in parts:
997
+ raise ValueError("Path cannot contain parent path segments")
998
+ return parts
999
+
1000
+ def _resolve_user(self, path: str) -> str:
1001
+ # Explicit read of the run user's Mon espace (AGENT-FILESYSTEM-RFC §7) — same
1002
+ # user the agent acts for; KF enforces own-uid ownership. v1 reads the whole
1003
+ # Mon espace; selection-scoping (§7.3) is deferred hardening, like G1b.
1004
+ return f"teams/{self._session_team()}/users/{self._session_user()}/" + "/".join(
1005
+ self._clean_parts(path)
1006
+ )
1007
+
1008
+ def _resolve_team(self, path: str) -> str:
1009
+ # Explicit read of the team's Espace d'equipe; governed by the user's team read.
1010
+ return f"teams/{self._session_team()}/shared/" + "/".join(
1011
+ self._clean_parts(path)
1012
+ )
951
1013
 
952
1014
  # ---- operations ----
953
- async def read_bytes(self, path: str) -> bytes:
1015
+ async def _download(self, resolved: str, original: str) -> bytes:
954
1016
  try:
955
1017
  blob = await self._workspace_client.fs_download_blob(
956
- self._resolve(path), self._token()
1018
+ resolved, self._token()
957
1019
  )
958
1020
  except WorkspaceRetrievalError as e:
959
1021
  if e.status_code == 404:
960
- raise WorkspaceFileNotFound(path) from e
1022
+ raise WorkspaceFileNotFound(original) from e
961
1023
  raise
962
1024
  return blob.bytes
963
1025
 
1026
+ async def read_bytes(self, path: str) -> bytes:
1027
+ return await self._download(self._resolve(path), path)
1028
+
964
1029
  async def read_text(self, path: str) -> str:
965
1030
  return (await self.read_bytes(path)).decode("utf-8")
966
1031
 
1032
+ async def read_user_bytes(self, path: str) -> bytes:
1033
+ return await self._download(self._resolve_user(path), path)
1034
+
1035
+ async def read_team_bytes(self, path: str) -> bytes:
1036
+ return await self._download(self._resolve_team(path), path)
1037
+
967
1038
  async def write(
968
1039
  self,
969
1040
  path: str,
@@ -972,7 +1043,7 @@ class FredWorkspaceFs(WorkspaceFsPort):
972
1043
  content_type: str | None = None,
973
1044
  title: str | None = None,
974
1045
  ) -> PublishedArtifact:
975
- resolved = self._resolve(path)
1046
+ resolved = self._resolve_owned(path)
976
1047
  file_name = resolved.rsplit("/", 1)[-1]
977
1048
  result = await self._workspace_client.fs_upload(
978
1049
  resolved, content, file_name, content_type
@@ -997,7 +1068,7 @@ class FredWorkspaceFs(WorkspaceFsPort):
997
1068
  ]
998
1069
 
999
1070
  async def delete(self, path: str) -> None:
1000
- await self._workspace_client.fs_delete(self._resolve(path), self._token())
1071
+ await self._workspace_client.fs_delete(self._resolve_owned(path), self._token())
1001
1072
 
1002
1073
  async def link_for(self, path: str) -> PublishedArtifact:
1003
1074
  resolved = self._resolve(path)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fred-runtime
3
- Version: 3.1.0
3
+ Version: 3.1.1
4
4
  Summary: Runtime adapters and infrastructure wiring for Fred v2 agents.
5
5
  Author-email: Thales <noreply@thalesgroup.com>
6
6
  License: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fred-runtime"
3
- version = "3.1.0"
3
+ version = "3.1.1"
4
4
  description = "Runtime adapters and infrastructure wiring for Fred v2 agents."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12,<3.13"
@@ -67,7 +67,10 @@ def _fs(client: _FakeClient | None = None) -> FredWorkspaceFs:
67
67
  fs = object.__new__(FredWorkspaceFs)
68
68
  fs._binding = SimpleNamespace( # type: ignore[assignment]
69
69
  runtime_context=SimpleNamespace(
70
- team_id="acme", user_id="u-1", access_token="tok"
70
+ team_id="acme",
71
+ user_id="u-1",
72
+ agent_instance_id="inst-7",
73
+ access_token="tok",
71
74
  )
72
75
  )
73
76
  fs._settings = SimpleNamespace(team_id="acme") # type: ignore[assignment]
@@ -78,8 +81,20 @@ def _fs(client: _FakeClient | None = None) -> FredWorkspaceFs:
78
81
  # ---- relativization (the §7.1 security rule) ----
79
82
 
80
83
 
81
- def test_resolve_bare_path_goes_to_user_private_space():
82
- assert _fs()._resolve("outputs/q3.pptx") == "teams/acme/users/u-1/outputs/q3.pptx"
84
+ def test_resolve_bare_path_goes_to_agent_space():
85
+ # FILES-04 §3/§6: a bare agent write lands in the agent's own per-user space,
86
+ # keyed by agent_instance_id — not Mon espace.
87
+ assert (
88
+ _fs()._resolve("outputs/q3.pptx")
89
+ == "teams/acme/agents/inst-7/users/u-1/outputs/q3.pptx"
90
+ )
91
+
92
+
93
+ def test_resolve_missing_agent_instance_raises():
94
+ fs = _fs()
95
+ fs._binding.runtime_context.agent_instance_id = None
96
+ with pytest.raises(RuntimeError, match="agent instance"):
97
+ fs._resolve("outputs/q3.pptx")
83
98
 
84
99
 
85
100
  def test_resolve_shared_prefix_goes_to_team_space():
@@ -107,7 +122,7 @@ def test_resolve_empty_requires_allow_root():
107
122
  fs = _fs()
108
123
  with pytest.raises(ValueError, match="empty"):
109
124
  fs._resolve("")
110
- assert fs._resolve("", allow_root=True) == "teams/acme/users/u-1"
125
+ assert fs._resolve("", allow_root=True) == "teams/acme/agents/inst-7/users/u-1"
111
126
 
112
127
 
113
128
  # ---- operations ----
@@ -120,6 +135,32 @@ async def test_read_bytes_maps_404_to_not_found():
120
135
  await fs.read_bytes("outputs/missing.bin")
121
136
 
122
137
 
138
+ @pytest.mark.asyncio
139
+ async def test_read_user_bytes_resolves_to_mon_espace():
140
+ # G7: explicit read of the run user's Mon espace.
141
+ client = _FakeClient()
142
+ fs = _fs(client)
143
+ await fs.read_user_bytes("templates/brand.pptx")
144
+ assert client.calls[-1] == (
145
+ "download",
146
+ "teams/acme/users/u-1/templates/brand.pptx",
147
+ "tok",
148
+ )
149
+
150
+
151
+ @pytest.mark.asyncio
152
+ async def test_read_team_bytes_resolves_to_shared():
153
+ # G7: explicit read of Espace d'equipe.
154
+ client = _FakeClient()
155
+ fs = _fs(client)
156
+ await fs.read_team_bytes("templates/brand.pptx")
157
+ assert client.calls[-1] == (
158
+ "download",
159
+ "teams/acme/shared/templates/brand.pptx",
160
+ "tok",
161
+ )
162
+
163
+
123
164
  @pytest.mark.asyncio
124
165
  async def test_read_bytes_resolves_and_returns_content():
125
166
  client = _FakeClient()
@@ -134,7 +175,7 @@ async def test_read_bytes_resolves_and_returns_content():
134
175
 
135
176
 
136
177
  @pytest.mark.asyncio
137
- async def test_write_uploads_to_user_space_and_returns_artifact():
178
+ async def test_write_uploads_to_agent_space_and_returns_artifact():
138
179
  client = _FakeClient()
139
180
  fs = _fs(client)
140
181
  artifact = await fs.write(
@@ -142,25 +183,65 @@ async def test_write_uploads_to_user_space_and_returns_artifact():
142
183
  )
143
184
  assert isinstance(artifact, PublishedArtifact)
144
185
  assert artifact.file_name == "q3.pptx"
145
- assert artifact.href == "/dl/teams/acme/users/u-1/outputs/q3.pptx"
146
- assert client.calls[-1][0:2] == ("upload", "teams/acme/users/u-1/outputs/q3.pptx")
186
+ assert artifact.href == "/dl/teams/acme/agents/inst-7/users/u-1/outputs/q3.pptx"
187
+ assert client.calls[-1][0:2] == (
188
+ "upload",
189
+ "teams/acme/agents/inst-7/users/u-1/outputs/q3.pptx",
190
+ )
147
191
 
148
192
 
149
193
  @pytest.mark.asyncio
150
- async def test_ls_lists_user_root_by_default():
194
+ async def test_ls_lists_agent_root_by_default():
151
195
  client = _FakeClient()
152
196
  fs = _fs(client)
153
197
  entries = await fs.ls()
154
198
  assert [e.path for e in entries] == ["deck.pptx"]
155
- assert client.calls[-1] == ("list", "teams/acme/users/u-1", "tok")
199
+ assert client.calls[-1] == ("list", "teams/acme/agents/inst-7/users/u-1", "tok")
156
200
 
157
201
 
158
202
  @pytest.mark.asyncio
159
- async def test_delete_resolves_path():
203
+ async def test_delete_resolves_owned_path():
160
204
  client = _FakeClient()
161
205
  fs = _fs(client)
162
- await fs.delete("shared/x.txt")
163
- assert client.calls[-1] == ("delete", "teams/acme/shared/x.txt", "tok")
206
+ await fs.delete("outputs/x.txt")
207
+ assert client.calls[-1] == (
208
+ "delete",
209
+ "teams/acme/agents/inst-7/users/u-1/outputs/x.txt",
210
+ "tok",
211
+ )
212
+
213
+
214
+ # ---- write/delete isolation: agents mutate only their own subtree (G2/G3) ----
215
+
216
+
217
+ @pytest.mark.asyncio
218
+ async def test_write_rejects_shared_path():
219
+ # G3: an agent cannot write into Espace d'equipe even though it can read it.
220
+ fs = _fs()
221
+ with pytest.raises(PermissionError, match="own space"):
222
+ await fs.write("shared/templates/brand.pptx", b"x")
223
+
224
+
225
+ @pytest.mark.asyncio
226
+ async def test_delete_rejects_shared_path():
227
+ fs = _fs()
228
+ with pytest.raises(PermissionError, match="own space"):
229
+ await fs.delete("shared/x.txt")
230
+
231
+
232
+ @pytest.mark.asyncio
233
+ async def test_write_rejects_sibling_agent_absolute_path():
234
+ # G2: an absolute path into another agent instance's subtree is rejected.
235
+ fs = _fs()
236
+ with pytest.raises(PermissionError, match="own space"):
237
+ await fs.write("/teams/acme/agents/inst-OTHER/users/u-1/outputs/x.pptx", b"x")
238
+
239
+
240
+ @pytest.mark.asyncio
241
+ async def test_write_rejects_cross_user_absolute_path():
242
+ fs = _fs()
243
+ with pytest.raises(PermissionError, match="own space"):
244
+ await fs.write("/teams/acme/agents/inst-7/users/u-OTHER/outputs/x.pptx", b"x")
164
245
 
165
246
 
166
247
  @pytest.mark.asyncio
@@ -170,10 +251,13 @@ async def test_link_for_resolves_and_returns_signed_artifact():
170
251
  artifact = await fs.link_for("uploads/report.xlsx")
171
252
  assert isinstance(artifact, PublishedArtifact)
172
253
  assert artifact.file_name == "report.xlsx"
173
- assert artifact.href == "/dl/teams/acme/users/u-1/uploads/report.xlsx?token=sig"
254
+ assert (
255
+ artifact.href
256
+ == "/dl/teams/acme/agents/inst-7/users/u-1/uploads/report.xlsx?token=sig"
257
+ )
174
258
  assert artifact.mime == "application/octet-stream"
175
259
  assert client.calls[-1] == (
176
260
  "share",
177
- "teams/acme/users/u-1/uploads/report.xlsx",
261
+ "teams/acme/agents/inst-7/users/u-1/uploads/report.xlsx",
178
262
  "tok",
179
263
  )
@@ -247,7 +247,9 @@ def test_fs_path_percent_encodes_reserved_chars_preserving_separators():
247
247
  # Reserved chars (#, ?, space) must be encoded so the {path:path} route is not
248
248
  # truncated, while "/" separators stay literal.
249
249
  assert (
250
- KfWorkspaceClient._fs_path("download", "teams/acme/users/u-1/outputs/Q3 #1?.txt")
250
+ KfWorkspaceClient._fs_path(
251
+ "download", "teams/acme/users/u-1/outputs/Q3 #1?.txt"
252
+ )
251
253
  == "/fs/download/teams/acme/users/u-1/outputs/Q3%20%231%3F.txt"
252
254
  )
253
255
 
File without changes
@@ -17,8 +17,8 @@ from __future__ import annotations
17
17
  import logging
18
18
  import re
19
19
  from dataclasses import dataclass
20
- from urllib.parse import quote
21
20
  from typing import BinaryIO, Callable
21
+ from urllib.parse import quote
22
22
 
23
23
  import httpx
24
24
 
File without changes