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.
- {gemcode-0.4.20/src/gemcode.egg-info → gemcode-0.4.21}/PKG-INFO +2 -1
- {gemcode-0.4.20 → gemcode-0.4.21}/README.md +1 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/pyproject.toml +1 -1
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/repl_commands.py +2 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/repl_slash.py +33 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/web/hitl_bridge.py +8 -4
- gemcode-0.4.21/src/gemcode/web/project_root.py +60 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/web/server.py +13 -4
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/web/sse_adapter.py +9 -6
- {gemcode-0.4.20 → gemcode-0.4.21/src/gemcode.egg-info}/PKG-INFO +2 -1
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode.egg-info/SOURCES.txt +1 -0
- gemcode-0.4.21/tests/test_hosted_tenant.py +52 -0
- gemcode-0.4.20/src/gemcode/web/project_root.py +0 -21
- {gemcode-0.4.20 → gemcode-0.4.21}/LICENSE +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/MANIFEST.in +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/setup.cfg +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/__init__.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/__main__.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/a2a_bridge.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/agent.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/agent_habits.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/agent_intelligence.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/agent_mesh.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/agent_triggers.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/audit.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/autocompact.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/automations.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/autotune.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/callbacks.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/capability_routing.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/checkpoints.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/cli.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/codebase_awareness.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/compaction.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/computer_use/__init__.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/computer_use/browser_computer.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/config.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/context_budget.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/context_warning.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/credentials.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/curated_memory.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/delegation_learning.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/dynamic_policy.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/evals/harness.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/event_bus.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/fleet_reports.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/hitl_session.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/hooks.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/ide_protocol.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/ide_stdio.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/intent_classifier.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/interactions.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/invoke.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/kaira_client.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/kaira_daemon.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/kaira_ipc.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/kaira_job_store.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/learning.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/limits.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/live_audio_engine.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/logging_config.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/mcp_loader.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/memory/__init__.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/memory/embedding_memory_service.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/memory/file_memory_service.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/modality_tools.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/model_errors.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/model_routing.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/multimodal_input.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/openapi_loader.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/org.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/output_styles.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/paths.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/permissions.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/plugins/__init__.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/policy_profile.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/pricing.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/prompt_suggestions.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/query/__init__.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/query/config.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/query/deps.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/query/engine.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/query/stop_hooks.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/query/token_budget.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/query/transitions.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/query_sanitizer.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/refine.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/review_agent.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/rules.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/self_healing.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/session_runtime.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/session_store.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/session_summariser.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/skills.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/slash_commands.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/thinking.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tool_prompt_manifest.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tool_registry.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tool_result_store.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tool_synthesis.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/__init__.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/automations_tools.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/bash.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/browser.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/compress_memory.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/curated_memory.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/edit.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/filesystem.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/notebook.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/notes.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/org_tools.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/repo_map.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/search.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/shell.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/shell_gate.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/skills.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/subtask.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/tasks.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/think.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/todo.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/user_choice.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/veomem_tools.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/web.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools/web_search.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tools_inspector.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/trust.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tui/input_handler.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tui/scrollback.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tui/spinner.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tui/welcome_banner.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/tui/welcome_rich.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/veomem_bridge.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/version.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/vertex.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/wal.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/web/__init__.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/web/chat_skills.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/web/customize_api.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/web/fleet_api.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/web/preview_api.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/web/runtime_api.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/web/serve_bind.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/web/serve_state.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/web/sessions_api.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/web/terminal_api.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/web/terminal_repl.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/web/web_config_api.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/web/web_sse_compat.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/web/workspace_panel_api.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode/workspace_hints.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode.egg-info/dependency_links.txt +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode.egg-info/entry_points.txt +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode.egg-info/requires.txt +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/src/gemcode.egg-info/top_level.txt +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_add_dir.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_agent_habits.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_agent_instruction.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_agent_mesh.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_autocompact.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_automations.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_capability_routing.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_checkpoint_diff_command.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_cli_init.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_compress_memory_tool.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_computer_use_permissions.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_context_budget.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_context_warning.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_credentials.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_eval_harness_layout.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_event_bus.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_fleet_reports.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_ide_stdio_attachments.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_interactive_permission_ask.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_kaira_ipc_paths.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_kaira_scheduler.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_modality_tools.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_model_error_retry.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_model_errors.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_model_routing.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_multimodal_input.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_output_styles_and_rules.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_paths.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_permissions.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_prompt_suggestions.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_repl_commands.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_repl_slash.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_session_runtime_cache.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_skills.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_slash_commands.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_slash_completion_registry.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_thinking_config.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_token_budget.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_tool_context_circulation.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_tools.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_tools_inspector.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_web_server.py +0 -0
- {gemcode-0.4.20 → gemcode-0.4.21}/tests/test_web_sse_adapter.py +0 -0
- {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.
|
|
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
|
|
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
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: {
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|