gemcode 0.4.20__tar.gz → 0.4.21__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 (200) hide show
  1. {gemcode-0.4.20/src/gemcode.egg-info → gemcode-0.4.21}/PKG-INFO +2 -1
  2. {gemcode-0.4.20 → gemcode-0.4.21}/README.md +1 -0
  3. {gemcode-0.4.20 → gemcode-0.4.21}/pyproject.toml +1 -1
  4. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/repl_commands.py +2 -0
  5. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/repl_slash.py +33 -0
  6. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/web/hitl_bridge.py +8 -4
  7. gemcode-0.4.21/src/gemcode/web/project_root.py +60 -0
  8. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/web/server.py +13 -4
  9. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/web/sse_adapter.py +9 -6
  10. {gemcode-0.4.20 → gemcode-0.4.21/src/gemcode.egg-info}/PKG-INFO +2 -1
  11. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode.egg-info/SOURCES.txt +1 -0
  12. gemcode-0.4.21/tests/test_hosted_tenant.py +52 -0
  13. gemcode-0.4.20/src/gemcode/web/project_root.py +0 -21
  14. {gemcode-0.4.20 → gemcode-0.4.21}/LICENSE +0 -0
  15. {gemcode-0.4.20 → gemcode-0.4.21}/MANIFEST.in +0 -0
  16. {gemcode-0.4.20 → gemcode-0.4.21}/setup.cfg +0 -0
  17. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/__init__.py +0 -0
  18. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/__main__.py +0 -0
  19. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/a2a_bridge.py +0 -0
  20. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/agent.py +0 -0
  21. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/agent_habits.py +0 -0
  22. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/agent_intelligence.py +0 -0
  23. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/agent_mesh.py +0 -0
  24. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/agent_triggers.py +0 -0
  25. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/audit.py +0 -0
  26. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/autocompact.py +0 -0
  27. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/automations.py +0 -0
  28. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/autotune.py +0 -0
  29. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/callbacks.py +0 -0
  30. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/capability_routing.py +0 -0
  31. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/checkpoints.py +0 -0
  32. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/cli.py +0 -0
  33. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/codebase_awareness.py +0 -0
  34. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/compaction.py +0 -0
  35. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/computer_use/__init__.py +0 -0
  36. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/computer_use/browser_computer.py +0 -0
  37. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/config.py +0 -0
  38. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/context_budget.py +0 -0
  39. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/context_warning.py +0 -0
  40. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/credentials.py +0 -0
  41. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/curated_memory.py +0 -0
  42. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/delegation_learning.py +0 -0
  43. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/dynamic_policy.py +0 -0
  44. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/evals/harness.py +0 -0
  45. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/event_bus.py +0 -0
  46. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/fleet_reports.py +0 -0
  47. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/hitl_session.py +0 -0
  48. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/hooks.py +0 -0
  49. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/ide_protocol.py +0 -0
  50. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/ide_stdio.py +0 -0
  51. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/intent_classifier.py +0 -0
  52. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/interactions.py +0 -0
  53. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/invoke.py +0 -0
  54. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/kaira_client.py +0 -0
  55. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/kaira_daemon.py +0 -0
  56. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/kaira_ipc.py +0 -0
  57. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/kaira_job_store.py +0 -0
  58. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/learning.py +0 -0
  59. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/limits.py +0 -0
  60. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/live_audio_engine.py +0 -0
  61. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/logging_config.py +0 -0
  62. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/mcp_loader.py +0 -0
  63. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/memory/__init__.py +0 -0
  64. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/memory/embedding_memory_service.py +0 -0
  65. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/memory/file_memory_service.py +0 -0
  66. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/modality_tools.py +0 -0
  67. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/model_errors.py +0 -0
  68. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/model_routing.py +0 -0
  69. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/multimodal_input.py +0 -0
  70. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/openapi_loader.py +0 -0
  71. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/org.py +0 -0
  72. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/output_styles.py +0 -0
  73. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/paths.py +0 -0
  74. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/permissions.py +0 -0
  75. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/plugins/__init__.py +0 -0
  76. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  77. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  78. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/policy_profile.py +0 -0
  79. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/pricing.py +0 -0
  80. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/prompt_suggestions.py +0 -0
  81. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/query/__init__.py +0 -0
  82. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/query/config.py +0 -0
  83. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/query/deps.py +0 -0
  84. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/query/engine.py +0 -0
  85. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/query/stop_hooks.py +0 -0
  86. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/query/token_budget.py +0 -0
  87. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/query/transitions.py +0 -0
  88. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/query_sanitizer.py +0 -0
  89. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/refine.py +0 -0
  90. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/review_agent.py +0 -0
  91. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/rules.py +0 -0
  92. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/self_healing.py +0 -0
  93. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/session_runtime.py +0 -0
  94. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/session_store.py +0 -0
  95. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/session_summariser.py +0 -0
  96. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/skills.py +0 -0
  97. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/slash_commands.py +0 -0
  98. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/thinking.py +0 -0
  99. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tool_prompt_manifest.py +0 -0
  100. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tool_registry.py +0 -0
  101. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tool_result_store.py +0 -0
  102. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tool_synthesis.py +0 -0
  103. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/__init__.py +0 -0
  104. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/automations_tools.py +0 -0
  105. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/bash.py +0 -0
  106. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/browser.py +0 -0
  107. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/compress_memory.py +0 -0
  108. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/curated_memory.py +0 -0
  109. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/edit.py +0 -0
  110. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/filesystem.py +0 -0
  111. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/notebook.py +0 -0
  112. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/notes.py +0 -0
  113. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/org_tools.py +0 -0
  114. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/repo_map.py +0 -0
  115. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/search.py +0 -0
  116. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/shell.py +0 -0
  117. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/shell_gate.py +0 -0
  118. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/skills.py +0 -0
  119. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/subtask.py +0 -0
  120. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/tasks.py +0 -0
  121. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/think.py +0 -0
  122. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/todo.py +0 -0
  123. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/user_choice.py +0 -0
  124. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/veomem_tools.py +0 -0
  125. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/web.py +0 -0
  126. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/web_search.py +0 -0
  127. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools_inspector.py +0 -0
  128. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/trust.py +0 -0
  129. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tui/input_handler.py +0 -0
  130. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tui/scrollback.py +0 -0
  131. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tui/spinner.py +0 -0
  132. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tui/welcome_banner.py +0 -0
  133. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tui/welcome_rich.py +0 -0
  134. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/veomem_bridge.py +0 -0
  135. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/version.py +0 -0
  136. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/vertex.py +0 -0
  137. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/wal.py +0 -0
  138. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/web/__init__.py +0 -0
  139. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/web/chat_skills.py +0 -0
  140. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/web/customize_api.py +0 -0
  141. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/web/fleet_api.py +0 -0
  142. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/web/preview_api.py +0 -0
  143. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/web/runtime_api.py +0 -0
  144. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/web/serve_bind.py +0 -0
  145. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/web/serve_state.py +0 -0
  146. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/web/sessions_api.py +0 -0
  147. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/web/terminal_api.py +0 -0
  148. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/web/terminal_repl.py +0 -0
  149. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/web/web_config_api.py +0 -0
  150. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/web/web_sse_compat.py +0 -0
  151. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/web/workspace_panel_api.py +0 -0
  152. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/workspace_hints.py +0 -0
  153. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode.egg-info/dependency_links.txt +0 -0
  154. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode.egg-info/entry_points.txt +0 -0
  155. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode.egg-info/requires.txt +0 -0
  156. {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode.egg-info/top_level.txt +0 -0
  157. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_add_dir.py +0 -0
  158. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_agent_habits.py +0 -0
  159. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_agent_instruction.py +0 -0
  160. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_agent_mesh.py +0 -0
  161. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_autocompact.py +0 -0
  162. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_automations.py +0 -0
  163. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_capability_routing.py +0 -0
  164. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_checkpoint_diff_command.py +0 -0
  165. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_cli_init.py +0 -0
  166. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_compress_memory_tool.py +0 -0
  167. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_computer_use_permissions.py +0 -0
  168. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_context_budget.py +0 -0
  169. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_context_warning.py +0 -0
  170. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_credentials.py +0 -0
  171. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_eval_harness_layout.py +0 -0
  172. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_event_bus.py +0 -0
  173. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_fleet_reports.py +0 -0
  174. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_ide_stdio_attachments.py +0 -0
  175. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_interactive_permission_ask.py +0 -0
  176. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_kaira_ipc_paths.py +0 -0
  177. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_kaira_scheduler.py +0 -0
  178. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_modality_tools.py +0 -0
  179. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_model_error_retry.py +0 -0
  180. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_model_errors.py +0 -0
  181. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_model_routing.py +0 -0
  182. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_multimodal_input.py +0 -0
  183. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_output_styles_and_rules.py +0 -0
  184. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_paths.py +0 -0
  185. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_permissions.py +0 -0
  186. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_prompt_suggestions.py +0 -0
  187. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_repl_commands.py +0 -0
  188. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_repl_slash.py +0 -0
  189. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_session_runtime_cache.py +0 -0
  190. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_skills.py +0 -0
  191. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_slash_commands.py +0 -0
  192. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_slash_completion_registry.py +0 -0
  193. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_thinking_config.py +0 -0
  194. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_token_budget.py +0 -0
  195. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_tool_context_circulation.py +0 -0
  196. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_tools.py +0 -0
  197. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_tools_inspector.py +0 -0
  198. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_web_server.py +0 -0
  199. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_web_sse_adapter.py +0 -0
  200. {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_workspace_hints.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.4.20
3
+ Version: 0.4.21
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -221,6 +221,7 @@ All state lives under `.gemcode/` in the project root. No external services requ
221
221
  | Agent Mesh | In-process multi-agent orchestration (automatic) |
222
222
  | GemCode Runtime | Optional always-on background scheduler (`gemcode runtime`; alias `gemcode kaira`) |
223
223
  | HTTP API (`gemcode serve`) | Built-in web/custom UI backend on `http://127.0.0.1:3001` — chat SSE, sessions, panel, HITL |
224
+ | Hosted multi-tenant | One `gemcode serve` per user with `GEMCODE_HOSTED_TENANT_ROOT` — see [`../docs/hosted.md`](../docs/hosted.md) |
224
225
  | A2A server | Cross-machine agent communication via Google A2A protocol |
225
226
  | Live audio (experimental) | Microphone-driven Gemini Live sessions |
226
227
 
@@ -28,6 +28,7 @@ All state lives under `.gemcode/` in the project root. No external services requ
28
28
  | Agent Mesh | In-process multi-agent orchestration (automatic) |
29
29
  | GemCode Runtime | Optional always-on background scheduler (`gemcode runtime`; alias `gemcode kaira`) |
30
30
  | HTTP API (`gemcode serve`) | Built-in web/custom UI backend on `http://127.0.0.1:3001` — chat SSE, sessions, panel, HITL |
31
+ | Hosted multi-tenant | One `gemcode serve` per user with `GEMCODE_HOSTED_TENANT_ROOT` — see [`../docs/hosted.md`](../docs/hosted.md) |
31
32
  | A2A server | Cross-machine agent communication via Google A2A protocol |
32
33
  | Live audio (experimental) | Microphone-driven Gemini Live sessions |
33
34
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gemcode"
7
- version = "0.4.20"
7
+ version = "0.4.21"
8
8
  description = "Local-first coding agent on Google Gemini + ADK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -251,6 +251,7 @@ SLASH_COMMANDS: list[tuple[str, str]] = [
251
251
  ("kaira", "Background jobs — gemcode runtime (alias: gemcode kaira)"),
252
252
  ("runtime", "Fleet socket status · gemcode runtime · attach/connect"),
253
253
  ("serve", "HTTP API for web/custom UIs — gemcode serve on :3001"),
254
+ ("hosted", "Hosted multi-tenant lock — GEMCODE_HOSTED_TENANT_ROOT status"),
254
255
  ("bus", "Runtime bus — send/publish lightweight messages over IPC"),
255
256
  ("inbox", "Bus inbox filters for this UI (to/topics)"),
256
257
  ("fleet", "Fleet inbox — /fleet show | digest (habits / mesh reports)"),
@@ -413,6 +414,7 @@ def slash_help_lines() -> list[str]:
413
414
  " /live-audio How to run gemcode live-audio (mic → Gemini Live)",
414
415
  " /doctor Environment sanity check",
415
416
  " /serve [start|status|stop|url] Start HTTP API for web UI (gemcode serve on :3001)",
417
+ " /hosted [status] Hosted tenant lock (GEMCODE_HOSTED_TENANT_ROOT)",
416
418
  " /version Print GemCode version hint",
417
419
  " /exit Exit the REPL",
418
420
  "",
@@ -2650,6 +2650,39 @@ async def process_repl_slash(
2650
2650
  out()
2651
2651
  return ReplSlashResult(skip_model_turn=True)
2652
2652
 
2653
+ # ── /hosted (multi-tenant serve lock) ───────────────────────────────────
2654
+ if name == "hosted":
2655
+ from gemcode.web.project_root import hosted_tenant_root
2656
+
2657
+ args_h = (sc.args or "").strip().lower()
2658
+ first_h = args_h.split()[0] if args_h else "status"
2659
+
2660
+ if first_h in ("help", "?"):
2661
+ out("Hosted multi-tenant mode (one workspace per gemcode serve process):")
2662
+ out(" /hosted Show lock status")
2663
+ out(" /hosted status Same")
2664
+ out("")
2665
+ out("Set GEMCODE_HOSTED_TENANT_ROOT to the tenant workspace path.")
2666
+ out("Web API then rejects client path/project_root outside that directory.")
2667
+ out("HITL approvals use {workspace}/.gemcode/web_approvals.")
2668
+ out("")
2669
+ out("Deployment guide: docs/hosted.md and deploy/gcp/ in the repo.")
2670
+ out()
2671
+ return ReplSlashResult(skip_model_turn=True)
2672
+
2673
+ locked = hosted_tenant_root()
2674
+ if locked is not None:
2675
+ out("[hosted] active — tenant workspace locked")
2676
+ out(f" GEMCODE_HOSTED_TENANT_ROOT={locked}")
2677
+ out(f" project_root: {cfg.project_root}")
2678
+ out(" Client path params must stay inside the tenant root (HTTP 403 otherwise).")
2679
+ else:
2680
+ out("[hosted] not active (local mode)")
2681
+ out(" Client path / project_root query params are honored.")
2682
+ out(" Set GEMCODE_HOSTED_TENANT_ROOT in containers for multi-tenant hosting.")
2683
+ out()
2684
+ return ReplSlashResult(skip_model_turn=True)
2685
+
2653
2686
  # ── /runtime ────────────────────────────────────────────────────────────
2654
2687
  if name == "runtime":
2655
2688
  from gemcode.kaira_ipc import fleet_manager_ipc_path_for_workspace
@@ -10,7 +10,11 @@ import uuid
10
10
  from pathlib import Path
11
11
  from typing import Any, Callable
12
12
 
13
- _APPROVAL_DIR = Path.home() / ".gemcode" / "web_approvals"
13
+ def _approval_dir() -> Path:
14
+ tenant = os.environ.get("GEMCODE_HOSTED_TENANT_ROOT", "").strip()
15
+ if tenant:
16
+ return Path(tenant).expanduser().resolve() / ".gemcode" / "web_approvals"
17
+ return Path.home() / ".gemcode" / "web_approvals"
14
18
  _DEFAULT_TIMEOUT_S = float(os.environ.get("GEMCODE_WEB_HITL_TIMEOUT_S", "3600"))
15
19
  _POLL_INTERVAL_S = 0.25
16
20
  # approval_ids currently blocked in wait_for_web_approval (same process)
@@ -28,7 +32,7 @@ def register_pending_approval(approval_id: str) -> None:
28
32
 
29
33
  def _approval_path(approval_id: str) -> Path:
30
34
  safe = "".join(c if c.isalnum() or c in "-_:" else "_" for c in approval_id)
31
- return _APPROVAL_DIR / f"{safe}.json"
35
+ return _approval_dir() / f"{safe}.json"
32
36
 
33
37
 
34
38
  def _read_approval_file(path: Path) -> bool | None:
@@ -54,7 +58,7 @@ def resolve_web_approval(approval_id: str, *, confirmed: bool) -> dict[str, Any]
54
58
  """Record the user's approve/deny decision (called from HTTP handler)."""
55
59
  if not approval_id.strip():
56
60
  return {"ok": False, "error": "approval_id is required"}
57
- _APPROVAL_DIR.mkdir(parents=True, exist_ok=True)
61
+ _approval_dir().mkdir(parents=True, exist_ok=True)
58
62
  path = _approval_path(approval_id)
59
63
  payload = {"confirmed": bool(confirmed), "resolved_ms": int(time.time() * 1000)}
60
64
  path.write_text(json.dumps(payload), encoding="utf-8")
@@ -75,7 +79,7 @@ async def wait_for_web_approval(
75
79
  heartbeat_s: float = 15.0,
76
80
  ) -> bool:
77
81
  """Block until the user approves/denies or timeout (deny on timeout)."""
78
- _APPROVAL_DIR.mkdir(parents=True, exist_ok=True)
82
+ _approval_dir().mkdir(parents=True, exist_ok=True)
79
83
  path = _approval_path(approval_id)
80
84
 
81
85
  # User may have approved before we started waiting (preflight UI is immediate).
@@ -0,0 +1,60 @@
1
+ """Shared workspace path resolution for GemCode web HTTP handlers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+
8
+
9
+ class HostedTenantPathError(ValueError):
10
+ """Client requested a path outside the locked tenant workspace."""
11
+
12
+
13
+ def hosted_tenant_root() -> Path | None:
14
+ """When set, web handlers are locked to this directory (multi-tenant hosting)."""
15
+ raw = os.environ.get("GEMCODE_HOSTED_TENANT_ROOT", "").strip()
16
+ if not raw:
17
+ return None
18
+ return Path(raw).expanduser().resolve()
19
+
20
+
21
+ def _is_within_root(root: Path, candidate: Path) -> bool:
22
+ try:
23
+ candidate.resolve().relative_to(root.resolve())
24
+ return True
25
+ except ValueError:
26
+ return False
27
+
28
+
29
+ def resolve_web_project_root(raw_path: str | None) -> Path:
30
+ """
31
+ Resolve the active project/workspace root for a web API request.
32
+
33
+ Prefers an explicit ``path`` query/body param, then ``GEMCODE_WEB_PROJECT_ROOT``,
34
+ then the API process working directory.
35
+
36
+ When ``GEMCODE_HOSTED_TENANT_ROOT`` is set (hosted multi-tenant mode), client
37
+ paths must stay inside that root; otherwise the locked root is always used.
38
+ """
39
+ locked = hosted_tenant_root()
40
+ if locked is not None:
41
+ if raw_path and str(raw_path).strip():
42
+ candidate = Path(str(raw_path).strip()).expanduser().resolve()
43
+ if not _is_within_root(locked, candidate):
44
+ raise HostedTenantPathError(
45
+ f"path is outside tenant workspace: {candidate} (root={locked})"
46
+ )
47
+ return candidate
48
+ return locked
49
+
50
+ if raw_path and str(raw_path).strip():
51
+ return Path(str(raw_path).strip()).expanduser().resolve()
52
+ env = os.environ.get("GEMCODE_WEB_PROJECT_ROOT", "").strip()
53
+ if env:
54
+ return Path(env).expanduser().resolve()
55
+ return Path.cwd().resolve()
56
+
57
+
58
+ def resolve_sse_project_root(raw_path: str | None) -> Path:
59
+ """Same rules as :func:`resolve_web_project_root` for SSE chat subprocess."""
60
+ return resolve_web_project_root(raw_path)
@@ -18,6 +18,7 @@ from pathlib import Path
18
18
  from typing import Any
19
19
  from urllib.parse import parse_qs, urlparse
20
20
 
21
+ from gemcode.web.project_root import HostedTenantPathError
21
22
  from gemcode.web.serve_bind import bind_http_server, format_connection_lines
22
23
  from gemcode.web.serve_state import (
23
24
  clear_serve_state,
@@ -182,8 +183,10 @@ def _build_handler(project_root: str) -> type[BaseHTTPRequestHandler]:
182
183
  except Exception:
183
184
  pass
184
185
  has_key = bool(os.environ.get("GOOGLE_API_KEY") or os.environ.get("GEMINI_API_KEY"))
185
- body = json.dumps(
186
- {
186
+ from gemcode.web.project_root import hosted_tenant_root
187
+
188
+ locked = hosted_tenant_root()
189
+ health: dict[str, Any] = {
187
190
  "status": "ok",
188
191
  "service": "gemcode-serve",
189
192
  "gemcode": True,
@@ -193,8 +196,11 @@ def _build_handler(project_root: str) -> type[BaseHTTPRequestHandler]:
193
196
  "cwd": root,
194
197
  "port": self.server.server_address[1],
195
198
  "url": serve_base_url(self.server.server_address[0], self.server.server_address[1]),
196
- }
197
- ).encode()
199
+ }
200
+ if locked is not None:
201
+ health["hosted_mode"] = True
202
+ health["hosted_tenant_root"] = str(locked)
203
+ body = json.dumps(health).encode()
198
204
  self.send_response(200)
199
205
  self.send_header("Content-Type", "application/json; charset=utf-8")
200
206
  self.send_header("Content-Length", str(len(body)))
@@ -371,6 +377,9 @@ def _build_handler(project_root: str) -> type[BaseHTTPRequestHandler]:
371
377
  else:
372
378
  raw_path = str(data.get("path") or root).strip()
373
379
  status, payload = handler(data, raw_path)
380
+ except HostedTenantPathError as exc:
381
+ payload = {"ok": False, "error": str(exc)}
382
+ status = 403
374
383
  except Exception as exc:
375
384
  payload = {"ok": False, "error": f"{type(exc).__name__}: {exc}"}
376
385
  status = 500
@@ -1061,14 +1061,17 @@ async def run_adapter(req: dict[str, Any]) -> None:
1061
1061
  if not prompt.strip():
1062
1062
  raise ValueError("No user message in request")
1063
1063
 
1064
+ from gemcode.web.project_root import HostedTenantPathError, resolve_sse_project_root
1065
+
1064
1066
  req_root = req.get("project_root")
1065
- if isinstance(req_root, str) and req_root.strip():
1066
- project_root = req_root.strip()
1067
- else:
1068
- project_root = os.environ.get("GEMCODE_WEB_PROJECT_ROOT") or os.getcwd()
1069
- root_path = Path(project_root).expanduser()
1067
+ raw_root = req_root.strip() if isinstance(req_root, str) and req_root.strip() else None
1068
+ try:
1069
+ root_path = resolve_sse_project_root(raw_root)
1070
+ except HostedTenantPathError as exc:
1071
+ raise ValueError(str(exc)) from exc
1070
1072
  if not root_path.is_dir():
1071
- raise ValueError(f"project_root is not a directory: {project_root}")
1073
+ raise ValueError(f"project_root is not a directory: {root_path}")
1074
+ project_root = str(root_path)
1072
1075
  from gemcode.org import resolve_fleet_root
1073
1076
 
1074
1077
  fleet_root = resolve_fleet_root(root_path.resolve())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.4.20
3
+ Version: 0.4.21
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -221,6 +221,7 @@ All state lives under `.gemcode/` in the project root. No external services requ
221
221
  | Agent Mesh | In-process multi-agent orchestration (automatic) |
222
222
  | GemCode Runtime | Optional always-on background scheduler (`gemcode runtime`; alias `gemcode kaira`) |
223
223
  | HTTP API (`gemcode serve`) | Built-in web/custom UI backend on `http://127.0.0.1:3001` — chat SSE, sessions, panel, HITL |
224
+ | Hosted multi-tenant | One `gemcode serve` per user with `GEMCODE_HOSTED_TENANT_ROOT` — see [`../docs/hosted.md`](../docs/hosted.md) |
224
225
  | A2A server | Cross-machine agent communication via Google A2A protocol |
225
226
  | Live audio (experimental) | Microphone-driven Gemini Live sessions |
226
227
 
@@ -167,6 +167,7 @@ tests/test_credentials.py
167
167
  tests/test_eval_harness_layout.py
168
168
  tests/test_event_bus.py
169
169
  tests/test_fleet_reports.py
170
+ tests/test_hosted_tenant.py
170
171
  tests/test_ide_stdio_attachments.py
171
172
  tests/test_interactive_permission_ask.py
172
173
  tests/test_kaira_ipc_paths.py
@@ -0,0 +1,52 @@
1
+ """Tests for hosted multi-tenant workspace path locking."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import tempfile
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+ from gemcode.web.project_root import (
12
+ HostedTenantPathError,
13
+ hosted_tenant_root,
14
+ resolve_web_project_root,
15
+ )
16
+
17
+
18
+ @pytest.fixture
19
+ def tenant_env(monkeypatch: pytest.MonkeyPatch) -> Path:
20
+ root = Path(tempfile.mkdtemp())
21
+ monkeypatch.setenv("GEMCODE_HOSTED_TENANT_ROOT", str(root))
22
+ return root
23
+
24
+
25
+ def test_hosted_tenant_root_from_env(tenant_env: Path) -> None:
26
+ assert hosted_tenant_root() == tenant_env.resolve()
27
+
28
+
29
+ def test_hosted_mode_ignores_empty_client_path(tenant_env: Path) -> None:
30
+ assert resolve_web_project_root(None) == tenant_env.resolve()
31
+ assert resolve_web_project_root("") == tenant_env.resolve()
32
+
33
+
34
+ def test_hosted_mode_allows_path_inside_tenant(tenant_env: Path) -> None:
35
+ sub = tenant_env / "projects" / "demo"
36
+ sub.mkdir(parents=True)
37
+ assert resolve_web_project_root(str(sub)) == sub.resolve()
38
+
39
+
40
+ def test_hosted_mode_rejects_path_outside_tenant(
41
+ tenant_env: Path, monkeypatch: pytest.MonkeyPatch
42
+ ) -> None:
43
+ outside = Path(tempfile.mkdtemp())
44
+ monkeypatch.setenv("GEMCODE_HOSTED_TENANT_ROOT", str(tenant_env))
45
+ with pytest.raises(HostedTenantPathError):
46
+ resolve_web_project_root(str(outside))
47
+
48
+
49
+ def test_normal_mode_uses_client_path(monkeypatch: pytest.MonkeyPatch) -> None:
50
+ monkeypatch.delenv("GEMCODE_HOSTED_TENANT_ROOT", raising=False)
51
+ outside = Path(tempfile.mkdtemp())
52
+ assert resolve_web_project_root(str(outside)) == outside.resolve()
@@ -1,21 +0,0 @@
1
- """Shared workspace path resolution for GemCode web HTTP handlers."""
2
-
3
- from __future__ import annotations
4
-
5
- import os
6
- from pathlib import Path
7
-
8
-
9
- def resolve_web_project_root(raw_path: str | None) -> Path:
10
- """
11
- Resolve the active project/workspace root for a web API request.
12
-
13
- Prefers an explicit ``path`` query/body param, then ``GEMCODE_WEB_PROJECT_ROOT``,
14
- then the API process working directory.
15
- """
16
- if raw_path and str(raw_path).strip():
17
- return Path(str(raw_path).strip()).expanduser().resolve()
18
- env = os.environ.get("GEMCODE_WEB_PROJECT_ROOT", "").strip()
19
- if env:
20
- return Path(env).expanduser().resolve()
21
- return Path.cwd().resolve()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes