ltcai 9.0.0 → 9.2.0

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 (189) hide show
  1. package/README.md +88 -46
  2. package/auto_setup.py +7 -853
  3. package/desktop/electron/README.md +9 -0
  4. package/desktop/electron/main.cjs +5 -3
  5. package/docs/CHANGELOG.md +178 -2
  6. package/docs/COMMUNITY_AND_PLUGINS.md +4 -2
  7. package/docs/DEVELOPMENT.md +53 -14
  8. package/docs/LEGACY_COMPATIBILITY.md +4 -4
  9. package/docs/ONBOARDING.md +10 -2
  10. package/docs/OPERATIONS.md +8 -4
  11. package/docs/PRODUCT_DIRECTION_REVIEW.md +4 -0
  12. package/docs/TRUST_MODEL.md +5 -2
  13. package/docs/WHY_LATTICE.md +15 -10
  14. package/docs/WORKFLOW_DESIGNER.md +22 -0
  15. package/docs/kg-schema.md +13 -2
  16. package/docs/mcp-tools.md +17 -6
  17. package/docs/privacy.md +19 -3
  18. package/docs/public-deploy.md +32 -3
  19. package/docs/security-model.md +46 -11
  20. package/lattice_brain/__init__.py +1 -1
  21. package/lattice_brain/archive.py +1 -6
  22. package/lattice_brain/context.py +66 -9
  23. package/lattice_brain/graph/_kg_fsutil.py +1 -5
  24. package/lattice_brain/graph/discovery_index.py +174 -20
  25. package/lattice_brain/graph/documents.py +44 -9
  26. package/lattice_brain/graph/ingest.py +47 -20
  27. package/lattice_brain/graph/provenance.py +13 -6
  28. package/lattice_brain/graph/retrieval.py +140 -39
  29. package/lattice_brain/graph/retrieval_docgen.py +37 -4
  30. package/lattice_brain/ingestion.py +4 -7
  31. package/lattice_brain/portability.py +28 -14
  32. package/lattice_brain/runtime/agent_runtime.py +5 -9
  33. package/lattice_brain/runtime/hooks.py +30 -13
  34. package/lattice_brain/runtime/multi_agent.py +27 -8
  35. package/lattice_brain/runtime/statuses.py +10 -0
  36. package/lattice_brain/utils.py +20 -2
  37. package/lattice_brain/workflow.py +1 -5
  38. package/latticeai/__init__.py +1 -1
  39. package/latticeai/api/admin.py +1 -1
  40. package/latticeai/api/agent_registry.py +10 -8
  41. package/latticeai/api/agents.py +22 -3
  42. package/latticeai/api/auth.py +78 -16
  43. package/latticeai/api/browser.py +303 -34
  44. package/latticeai/api/chat.py +320 -850
  45. package/latticeai/api/chat_agent_http.py +356 -0
  46. package/latticeai/api/chat_contracts.py +54 -0
  47. package/latticeai/api/chat_documents.py +256 -0
  48. package/latticeai/api/chat_helpers.py +28 -1
  49. package/latticeai/api/chat_history.py +99 -0
  50. package/latticeai/api/chat_intents.py +316 -0
  51. package/latticeai/api/chat_stream.py +115 -0
  52. package/latticeai/api/computer_use.py +62 -9
  53. package/latticeai/api/health.py +9 -3
  54. package/latticeai/api/hooks.py +17 -6
  55. package/latticeai/api/knowledge_graph.py +78 -14
  56. package/latticeai/api/local_files.py +7 -2
  57. package/latticeai/api/mcp.py +102 -23
  58. package/latticeai/api/models.py +72 -33
  59. package/latticeai/api/network.py +5 -5
  60. package/latticeai/api/permissions.py +67 -26
  61. package/latticeai/api/plugins.py +21 -7
  62. package/latticeai/api/portability.py +5 -5
  63. package/latticeai/api/realtime.py +33 -5
  64. package/latticeai/api/setup.py +2 -2
  65. package/latticeai/api/static_routes.py +123 -24
  66. package/latticeai/api/tools.py +96 -14
  67. package/latticeai/api/workflow_designer.py +37 -0
  68. package/latticeai/api/workspace.py +2 -1
  69. package/latticeai/app_factory.py +110 -52
  70. package/latticeai/core/agent.py +50 -13
  71. package/latticeai/core/agent_prompts.py +5 -0
  72. package/latticeai/core/agent_registry.py +1 -5
  73. package/latticeai/core/config.py +23 -3
  74. package/latticeai/core/context_builder.py +21 -3
  75. package/latticeai/core/file_generation.py +451 -0
  76. package/latticeai/core/invitations.py +1 -4
  77. package/latticeai/core/io_utils.py +9 -1
  78. package/latticeai/core/legacy_compatibility.py +24 -3
  79. package/latticeai/core/marketplace.py +1 -1
  80. package/latticeai/core/plugins.py +15 -0
  81. package/latticeai/core/policy.py +1 -1
  82. package/latticeai/core/realtime.py +38 -15
  83. package/latticeai/core/sessions.py +7 -2
  84. package/latticeai/core/timeutil.py +10 -0
  85. package/latticeai/core/tool_registry.py +90 -18
  86. package/latticeai/core/users.py +1 -5
  87. package/latticeai/core/workspace_graph_trace.py +31 -5
  88. package/latticeai/core/workspace_memory.py +3 -1
  89. package/latticeai/core/workspace_os.py +18 -7
  90. package/latticeai/core/workspace_os_utils.py +0 -5
  91. package/latticeai/core/workspace_permissions.py +2 -1
  92. package/latticeai/core/workspace_plugins.py +1 -1
  93. package/latticeai/core/workspace_runs.py +14 -5
  94. package/latticeai/core/workspace_skills.py +1 -1
  95. package/latticeai/core/workspace_snapshots.py +2 -1
  96. package/latticeai/core/workspace_timeline.py +2 -1
  97. package/latticeai/integrations/telegram_bot.py +94 -43
  98. package/latticeai/models/router.py +130 -36
  99. package/latticeai/runtime/access_runtime.py +62 -4
  100. package/latticeai/runtime/automation_runtime.py +13 -7
  101. package/latticeai/runtime/brain_runtime.py +19 -7
  102. package/latticeai/runtime/chat_wiring.py +2 -0
  103. package/latticeai/runtime/config_runtime.py +58 -26
  104. package/latticeai/runtime/context_runtime.py +16 -4
  105. package/latticeai/runtime/hooks_runtime.py +1 -1
  106. package/latticeai/runtime/model_wiring.py +33 -5
  107. package/latticeai/runtime/namespace_runtime.py +62 -72
  108. package/latticeai/runtime/platform_runtime_wiring.py +4 -3
  109. package/latticeai/runtime/review_wiring.py +1 -1
  110. package/latticeai/runtime/router_registration.py +34 -1
  111. package/latticeai/runtime/security_runtime.py +110 -13
  112. package/latticeai/runtime/stages.py +27 -0
  113. package/latticeai/server_app.py +5 -1
  114. package/latticeai/services/architecture_readiness.py +74 -5
  115. package/latticeai/services/brain_automation.py +3 -26
  116. package/latticeai/services/chat_service.py +205 -15
  117. package/latticeai/services/local_knowledge.py +423 -0
  118. package/latticeai/services/memory_service.py +55 -21
  119. package/latticeai/services/model_engines.py +48 -33
  120. package/latticeai/services/model_errors.py +17 -0
  121. package/latticeai/services/model_loading.py +28 -25
  122. package/latticeai/services/model_runtime.py +228 -162
  123. package/latticeai/services/p_reinforce.py +22 -3
  124. package/latticeai/services/platform_runtime.py +83 -23
  125. package/latticeai/services/product_readiness.py +19 -17
  126. package/latticeai/services/review_queue.py +12 -0
  127. package/latticeai/services/router_context.py +1 -0
  128. package/latticeai/services/run_executor.py +4 -9
  129. package/latticeai/services/search_service.py +203 -28
  130. package/latticeai/services/tool_dispatch.py +28 -5
  131. package/latticeai/services/triggers.py +53 -4
  132. package/latticeai/services/upload_service.py +13 -2
  133. package/latticeai/setup/__init__.py +25 -0
  134. package/latticeai/setup/auto_setup.py +857 -0
  135. package/latticeai/setup/wizard.py +1264 -0
  136. package/latticeai/tools/__init__.py +278 -0
  137. package/{tools → latticeai/tools}/commands.py +67 -7
  138. package/{tools → latticeai/tools}/computer.py +1 -1
  139. package/{tools → latticeai/tools}/documents.py +1 -1
  140. package/{tools → latticeai/tools}/filesystem.py +3 -3
  141. package/latticeai/tools/knowledge.py +178 -0
  142. package/{tools → latticeai/tools}/local_files.py +1 -1
  143. package/{tools → latticeai/tools}/network.py +0 -1
  144. package/local_knowledge_api.py +4 -342
  145. package/package.json +22 -2
  146. package/scripts/brain_quality_eval.py +3 -1
  147. package/scripts/bump_version.py +3 -0
  148. package/scripts/capture_release_evidence.mjs +180 -0
  149. package/scripts/check_current_release_docs.mjs +142 -0
  150. package/scripts/check_i18n_literals.mjs +107 -31
  151. package/scripts/check_openapi_drift.mjs +56 -0
  152. package/scripts/export_openapi.py +67 -7
  153. package/scripts/i18n_literal_allowlist.json +22 -24
  154. package/scripts/run_integration_tests.mjs +72 -10
  155. package/scripts/validate_release_artifacts.py +49 -7
  156. package/scripts/wheel_smoke.py +5 -0
  157. package/setup_wizard.py +3 -1260
  158. package/src-tauri/Cargo.lock +1 -1
  159. package/src-tauri/Cargo.toml +1 -1
  160. package/src-tauri/tauri.conf.json +1 -1
  161. package/static/app/asset-manifest.json +11 -11
  162. package/static/app/assets/Act-C3dBrWE-.js +1 -0
  163. package/static/app/assets/Brain-DBYgdcjt.js +321 -0
  164. package/static/app/assets/Capture-Cf3hqRtN.js +1 -0
  165. package/static/app/assets/Library-CFfkNn3s.js +1 -0
  166. package/static/app/assets/System-BOurbT-v.js +1 -0
  167. package/static/app/assets/index-A3M9sElj.js +17 -0
  168. package/static/app/assets/index-Bmx9rzTc.css +2 -0
  169. package/static/app/assets/primitives-DcUUmhdC.js +1 -0
  170. package/static/app/assets/textarea-BklR6zN4.js +1 -0
  171. package/static/app/index.html +2 -2
  172. package/static/sw.js +1 -1
  173. package/tools/__init__.py +21 -269
  174. package/docs/CODE_REVIEW_2026-07-06.md +0 -764
  175. package/latticeai/runtime/app_context_runtime.py +0 -13
  176. package/latticeai/runtime/tail_wiring.py +0 -21
  177. package/scripts/com.pts.claudecode.discord.plist +0 -31
  178. package/scripts/pts-claudecode-discord-bridge.mjs +0 -207
  179. package/scripts/start-pts-claudecode-discord.sh +0 -51
  180. package/static/app/assets/Act-21lIXx2E.js +0 -1
  181. package/static/app/assets/Brain-BqUd5UJJ.js +0 -321
  182. package/static/app/assets/Capture-BA7Z2Q1u.js +0 -1
  183. package/static/app/assets/Library-bFMtyni3.js +0 -1
  184. package/static/app/assets/System-K6krGCqn.js +0 -1
  185. package/static/app/assets/index-C4R3ws30.js +0 -17
  186. package/static/app/assets/index-ChSeOB02.css +0 -2
  187. package/static/app/assets/primitives-sQU3it5I.js +0 -1
  188. package/static/app/assets/textarea-DK3Fd_lR.js +0 -1
  189. package/tools/knowledge.py +0 -95
@@ -18,7 +18,6 @@ import threading
18
18
  from dataclasses import dataclass
19
19
  from typing import TYPE_CHECKING, Any, Dict, List, Optional
20
20
 
21
- from latticeai.runtime.app_context_runtime import build_app_context
22
21
  from latticeai.runtime.access_runtime import build_access_runtime
23
22
  from latticeai.runtime.bootstrap import build_session_runtime
24
23
  from latticeai.runtime.brain_runtime import build_brain_runtime
@@ -52,6 +51,7 @@ from latticeai.runtime.sso_config_runtime import build_sso_config_runtime
52
51
  from latticeai.runtime.audit_runtime import build_audit_runtime
53
52
  from latticeai.runtime.router_registration import (
54
53
  build_auth_admin_security_router_bundle,
54
+ build_router_bundle,
55
55
  build_static_routes_bundle,
56
56
  register_health_and_model_routers,
57
57
  register_foundation_routers,
@@ -60,7 +60,6 @@ from latticeai.runtime.router_registration import (
60
60
  register_review_and_brain_tail_routers,
61
61
  )
62
62
  from latticeai.runtime.security_runtime import build_security_runtime
63
- from latticeai.runtime.tail_wiring import register_tail_runtime_routers
64
63
  from latticeai.runtime.user_key_runtime import build_user_key_runtime
65
64
  from latticeai.runtime.web_runtime import build_web_runtime
66
65
 
@@ -107,7 +106,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
107
106
  enforce_rate_limit as _enforce_rate_limit,
108
107
  )
109
108
  from latticeai.core.audit import (
110
- get_audit_log as _get_audit_log, # noqa: F401 - legacy server_app attr exported via locals()
109
+ get_audit_log as _get_audit_log, # noqa: F401 - explicit legacy server_app export
111
110
  classify_sensitive_message as _classify_sensitive_message,
112
111
  build_sensitivity_report as _build_sensitivity_report,
113
112
  build_admin_audit_report as _build_admin_audit_report,
@@ -132,24 +131,19 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
132
131
  user_id_for_email as _user_id_for_email,
133
132
  )
134
133
  from latticeai.services.chat_service import ChatService
134
+ from latticeai.services.app_context import AppContext
135
135
  from latticeai.core.embedding_providers import resolve_embedder, resolve_embedding_profile
136
136
  from latticeai.services.model_runtime import (
137
137
  CLOUD_VERIFY_TTL_SECONDS,
138
138
  ENGINE_MODEL_CATALOG,
139
139
  LOCAL_SERVER_PROCESSES,
140
140
  MODEL_ENGINE_ALIASES,
141
- configure_model_runtime,
141
+ build_model_runtime,
142
142
  download_hf_model,
143
- engine_status,
144
143
  filter_lower_family_versions,
145
- install_engine,
146
144
  local_binary,
147
145
  normalize_local_model_request,
148
- prepare_and_load_model,
149
- prepare_and_load_model_stream,
150
- runtime_features,
151
146
  sse_event,
152
- verify_cloud_models,
153
147
  ensure_ollama_server,
154
148
  )
155
149
  from latticeai.api.workspace import create_workspace_router, _workspace_scope_from_request
@@ -177,10 +171,8 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
177
171
  from lattice_brain.ingestion import IngestionItem
178
172
  from lattice_brain.storage import storage_from_env
179
173
  from latticeai.api.network import create_network_router
180
- # The aliased names below look unused but are part of the legacy
181
- # ``server_app`` attribute surface: every local is exported via
182
- # ``dict(locals())`` and reached through ``server_app.__getattr__``
183
- # (tests import _agent_risk, _LOCAL_WRITE_BLOCKED_PREFIXES, …).
174
+ # The aliased names below form the explicit, allowlisted compatibility
175
+ # surface consumed by historical ``server_app`` callers.
184
176
  from latticeai.services.tool_dispatch import ( # noqa: F401
185
177
  LOCAL_WRITE_BLOCKED_PREFIXES as _LOCAL_WRITE_BLOCKED_PREFIXES,
186
178
  TOOL_GOVERNANCE,
@@ -201,8 +193,8 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
201
193
  create_mcp_install_state,
202
194
  )
203
195
  from latticeai.services.p_reinforce import PReinforceGardener
204
- from setup_wizard import get_recommendations, scan_environment
205
- from tools import ensure_agent_root, execute_tool, knowledge_save
196
+ from latticeai.setup.wizard import get_recommendations, scan_environment
197
+ from latticeai.tools import ensure_agent_root, execute_tool, knowledge_save
206
198
 
207
199
  try:
208
200
  import keyring
@@ -229,7 +221,6 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
229
221
  def _host_is_loopback(host: str) -> bool:
230
222
  return _host_is_loopback_impl(host)
231
223
 
232
- NETWORK_EXPOSED = _config_runtime["NETWORK_EXPOSED"]
233
224
  ENABLE_TELEGRAM = _config_runtime["ENABLE_TELEGRAM"]
234
225
  ENABLE_GRAPH = _config_runtime["ENABLE_GRAPH"]
235
226
  AUTOLOAD_MODELS = _config_runtime["AUTOLOAD_MODELS"]
@@ -253,6 +244,10 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
253
244
  SSO_CLIENT_SECRET = _security_runtime["SSO_CLIENT_SECRET"]
254
245
  SSO_REDIRECT_URI = _security_runtime["SSO_REDIRECT_URI"]
255
246
  SSO_PROVIDER_NAME = _security_runtime["SSO_PROVIDER_NAME"]
247
+ INVITE_CODE = _security_runtime["INVITE_CODE"]
248
+ INVITE_COOKIE_SECRET = _security_runtime["INVITE_COOKIE_SECRET"]
249
+ INVITE_GATE_ENABLED = _security_runtime["INVITE_GATE_ENABLED"]
250
+ SECURE_COOKIES = _security_runtime["SECURE_COOKIES"]
256
251
 
257
252
  # SSO config + discovery seam is built below, after SSO_FILE is known
258
253
  # (latticeai.runtime.sso_config_runtime).
@@ -291,26 +286,27 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
291
286
  _session_store = _session_runtime["_session_store"]
292
287
  create_session = _session_runtime["create_session"]
293
288
  get_session_email = _session_runtime["get_session_email"]
294
- get_session_user_id = _session_runtime["get_session_user_id"]
295
289
  invalidate_session = _session_runtime["invalidate_session"]
296
290
 
297
291
  # ── User Management Logic ──────────────────────────────────────────────────
298
292
  BASE_DIR = Path(__file__).resolve().parent.parent
299
293
  DATA_DIR = CONFIG.data_dir
300
- DATA_DIR.mkdir(parents=True, exist_ok=True)
294
+ DATA_DIR.mkdir(parents=True, exist_ok=True, mode=0o700)
295
+ try:
296
+ DATA_DIR.chmod(0o700)
297
+ except OSError:
298
+ pass
301
299
  STATIC_DIR = CONFIG.static_dir
302
300
 
303
301
  USERS_FILE = DATA_DIR / "users.json"
304
302
  HISTORY_FILE = DATA_DIR / "chat_history.json"
305
303
  VPC_FILE = DATA_DIR / "vpc_config.json"
306
- MCP_FILE = DATA_DIR / "mcp_installs.json"
307
304
  AUDIT_FILE = DATA_DIR / "audit_log.json"
308
305
  SSO_FILE = DATA_DIR / "sso_config.json"
309
306
 
310
307
  # MCP state extracted to mcp_registry.create_mcp_install_state (server decomp)
311
308
  _mcp_state = create_mcp_install_state(DATA_DIR)
312
309
  load_mcp_installs = _mcp_state["load_mcp_installs"]
313
- save_mcp_installs = _mcp_state["save_mcp_installs"]
314
310
  mcp_public_item = _mcp_state["mcp_public_item"]
315
311
  recommend_mcps = _mcp_state["recommend_mcps"]
316
312
  install_mcp = _mcp_state["install_mcp"]
@@ -350,7 +346,6 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
350
346
  embedder=EMBEDDER,
351
347
  storage_engine=STORAGE_ENGINE,
352
348
  )
353
- BRAIN_CORE = _brain_runtime["BRAIN_CORE"]
354
349
  KNOWLEDGE_GRAPH = _brain_runtime["KNOWLEDGE_GRAPH"]
355
350
  # ── v4 durable conversation store: unbounded episodic memory in the same
356
351
  # SQLite file as the graph (kg_portability backup/restore covers it for
@@ -384,7 +379,6 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
384
379
  WORKSPACE_OS = _persistence_runtime["WORKSPACE_OS"]
385
380
  WORKSPACE_SERVICE = _persistence_runtime["WORKSPACE_SERVICE"]
386
381
  INVITATION_STORE = _persistence_runtime["INVITATION_STORE"]
387
- PLUGINS_DIR = _persistence_runtime["PLUGINS_DIR"]
388
382
  PLUGIN_REGISTRY = _persistence_runtime["PLUGIN_REGISTRY"]
389
383
  TEMPLATE_CATALOG = _persistence_runtime["TEMPLATE_CATALOG"]
390
384
  AGENT_REGISTRY = _persistence_runtime["AGENT_REGISTRY"]
@@ -442,7 +436,6 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
442
436
  logging=logging,
443
437
  )
444
438
  _sso_env_defaults = _sso_runtime["_sso_env_defaults"]
445
- load_sso_config = _sso_runtime["load_sso_config"]
446
439
  get_sso_settings = _sso_runtime["get_sso_settings"]
447
440
  public_sso_config = _sso_runtime["public_sso_config"]
448
441
  save_sso_config = _sso_runtime["save_sso_config"]
@@ -452,7 +445,6 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
452
445
  # MCP/skill request models moved to latticeai.api.mcp (v1.3.0).
453
446
  # VPC network-profile config → latticeai.runtime.network_config_runtime.
454
447
  _vpc_runtime = build_vpc_runtime(vpc_file=VPC_FILE, logging=logging)
455
- DEFAULT_VPC_CONFIG = _vpc_runtime["DEFAULT_VPC_CONFIG"]
456
448
  load_vpc_config = _vpc_runtime["load_vpc_config"]
457
449
  save_vpc_config = _vpc_runtime["save_vpc_config"]
458
450
 
@@ -579,10 +571,6 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
579
571
  clear_history = _history_query_runtime["clear_history"]
580
572
  clear_conversation = _history_query_runtime["clear_conversation"]
581
573
 
582
- # Chat service seam: behaviour-preserving façade for history access and
583
- # Workspace-OS answer-trace recording used by the (unchanged) streaming chat path.
584
- CHAT_SERVICE = ChatService(store=WORKSPACE_OS, get_history=get_history)
585
-
586
574
  _access_runtime = build_access_runtime(
587
575
  config=CONFIG,
588
576
  require_auth=REQUIRE_AUTH,
@@ -628,6 +616,15 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
628
616
  get_user_api_key = _user_key_runtime["get_user_api_key"]
629
617
  set_user_api_key = _user_key_runtime["set_user_api_key"]
630
618
 
619
+ # Chat service owns persistence and trace behavior after its user-key
620
+ # dependencies are available.
621
+ CHAT_SERVICE = ChatService(
622
+ store=WORKSPACE_OS,
623
+ get_history=get_history,
624
+ save_to_history=save_to_history,
625
+ get_history_user=get_history_user,
626
+ )
627
+
631
628
  # ── Sensitivity analysis — delegated to latticeai.core.audit ──────────────────
632
629
  def classify_sensitive_message(item: Dict, index: int) -> Dict:
633
630
  return _classify_sensitive_message(item, index)
@@ -684,8 +681,6 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
684
681
  local_server_processes=LOCAL_SERVER_PROCESSES,
685
682
  logger=logging,
686
683
  )
687
- autoload_default_model = _lifespan_runtime["autoload_default_model"]
688
- unload_idle_models_loop = _lifespan_runtime["unload_idle_models_loop"]
689
684
  _spawn = _lifespan_runtime["_spawn"]
690
685
  lifespan = _lifespan_runtime["lifespan"]
691
686
 
@@ -700,14 +695,11 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
700
695
  static_dir=STATIC_DIR,
701
696
  )
702
697
  app = _web_runtime["app"]
703
- CORS_ALLOWED_ORIGINS = _web_runtime["CORS_ALLOWED_ORIGINS"]
704
698
  ensure_agent_root()
705
699
 
706
700
  OPEN_REGISTRATION = CONFIG.open_registration
707
- INVITE_CODE = CONFIG.invite_code
708
- INVITE_GATE_ENABLED = CONFIG.invite_gate_enabled
709
- configure_model_runtime_from_context(
710
- configure_model_runtime=configure_model_runtime,
701
+ _model_runtime_service = configure_model_runtime_from_context(
702
+ build_model_runtime=build_model_runtime,
711
703
  router=router,
712
704
  APP_MODE=APP_MODE,
713
705
  DEFAULT_HOST=DEFAULT_HOST,
@@ -737,6 +729,8 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
737
729
  static_dir=STATIC_DIR,
738
730
  invite_gate_enabled=INVITE_GATE_ENABLED,
739
731
  invite_code=INVITE_CODE,
732
+ invite_cookie_secret=INVITE_COOKIE_SECRET,
733
+ secure_cookies=SECURE_COOKIES,
740
734
  app_mode=APP_MODE,
741
735
  model_router=router,
742
736
  require_user=require_user,
@@ -744,6 +738,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
744
738
  STATIC_ROUTES = _static_routes_bundle["STATIC_ROUTES"]
745
739
  ui_file_response = _static_routes_bundle["ui_file_response"]
746
740
  local_sysinfo = _static_routes_bundle["local_sysinfo"]
741
+ invite_authorized = _static_routes_bundle["invite_authorized"]
747
742
 
748
743
  # ── Auth & Admin routers (latticeai.api) ─────────────────────────────────────
749
744
  _foundation_router_bundle = build_auth_admin_security_router_bundle(
@@ -766,6 +761,8 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
766
761
  open_registration=OPEN_REGISTRATION,
767
762
  session_ttl=_SESSION_TTL,
768
763
  require_auth=REQUIRE_AUTH,
764
+ secure_cookies=SECURE_COOKIES,
765
+ invite_authorized=invite_authorized,
769
766
  ensure_identity=ensure_user_identity,
770
767
  create_admin_router=create_admin_router,
771
768
  require_admin=require_admin,
@@ -866,6 +863,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
866
863
  include_image_missing_replies: bool = True,
867
864
  user_email: Optional[str] = None,
868
865
  conversation_id: Optional[str] = None,
866
+ workspace_id: Optional[str] = None,
869
867
  ) -> str:
870
868
  return build_recent_chat_context(
871
869
  get_history=get_history,
@@ -873,6 +871,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
873
871
  include_image_missing_replies=include_image_missing_replies,
874
872
  user_email=user_email,
875
873
  conversation_id=conversation_id,
874
+ workspace_id=workspace_id,
876
875
  )
877
876
 
878
877
  CHAT_AGENT_RUNTIME = build_chat_agent_runtime_from_context(
@@ -889,7 +888,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
889
888
 
890
889
  # ── Typed dependency context (latticeai.services.app_context) ────────────────
891
890
  # One context object replaces the historical 25-30-kwarg router wiring.
892
- context = build_app_context(
891
+ context = AppContext(
893
892
  config=CONFIG,
894
893
  data_dir=DATA_DIR,
895
894
  static_dir=STATIC_DIR,
@@ -971,6 +970,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
971
970
  agent_registry=AGENT_REGISTRY,
972
971
  data_dir=DATA_DIR,
973
972
  append_audit_event=append_audit_event,
973
+ memory_service=MEMORY_SERVICE,
974
974
  tz_name=getattr(CONFIG, "timezone", None),
975
975
  )
976
976
  _llm_generate_sync = _platform_automation_runtime["_llm_generate_sync"]
@@ -1025,28 +1025,30 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1025
1025
  # ── Health / status / engine-summary router (latticeai.api.health, v1.2.0) ───
1026
1026
  # /health, /mode, /runtime_features, /engines(GET) now live in the health router.
1027
1027
  # Heavier engine mutation endpoints remain below in server_app.
1028
- MODEL_SERVICE = register_model_runtime_routers(
1028
+ _model_runtime = register_model_runtime_routers(
1029
1029
  app=app,
1030
1030
  create_health_router=create_health_router,
1031
1031
  create_models_router=create_models_router,
1032
1032
  register_health_and_model_routers=register_health_and_model_routers,
1033
1033
  model_router=router,
1034
- runtime_features=runtime_features,
1034
+ runtime_service=_model_runtime_service,
1035
+ runtime_features=_model_runtime_service.runtime_features,
1035
1036
  is_public_mode=IS_PUBLIC_MODE,
1036
- engine_status=engine_status,
1037
+ engine_status=_model_runtime_service.engine_status,
1037
1038
  get_current_user=get_current_user,
1038
1039
  require_auth=REQUIRE_AUTH,
1039
1040
  app_version=APP_VERSION,
1040
1041
  app_mode=APP_MODE,
1041
1042
  require_user=require_user,
1043
+ require_admin=require_admin,
1042
1044
  load_users=load_users,
1043
1045
  get_user_role=get_user_role,
1044
- install_engine=install_engine,
1045
- verify_cloud_models=verify_cloud_models,
1046
+ install_engine=_model_runtime_service.install_engine,
1047
+ verify_cloud_models=_model_runtime_service.verify_cloud_models,
1046
1048
  normalize_local_model_request=normalize_local_model_request,
1047
1049
  download_hf_model=download_hf_model,
1048
- prepare_and_load_model=prepare_and_load_model,
1049
- prepare_and_load_model_stream=prepare_and_load_model_stream,
1050
+ prepare_and_load_model=_model_runtime_service.prepare_and_load_model,
1051
+ prepare_and_load_model_stream=_model_runtime_service.prepare_and_load_model_stream,
1050
1052
  sse_event=sse_event,
1051
1053
  ensure_ollama_server=ensure_ollama_server,
1052
1054
  local_binary=local_binary,
@@ -1073,7 +1075,8 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1073
1075
 
1074
1076
  def _allowed_workspaces_for(user):
1075
1077
  # No-auth local mode is single-user: no scoping. With auth, scope
1076
- # reads to the caller's memberships (legacy-global rows stay visible).
1078
+ # reads to the caller's memberships (legacy-global rows require an
1079
+ # explicit maintenance-only compatibility opt-in).
1077
1080
  if not REQUIRE_AUTH or not user:
1078
1081
  return None
1079
1082
  return PLATFORM.allowed_scopes(user)
@@ -1102,6 +1105,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1102
1105
  install_mcp=install_mcp,
1103
1106
  mcp_public_item=mcp_public_item,
1104
1107
  hooks=HOOKS_REGISTRY,
1108
+ workspace_service=WORKSPACE_SERVICE,
1105
1109
  chat_context=context,
1106
1110
  search_service=SEARCH_SERVICE,
1107
1111
  allowed_workspaces_for=_allowed_workspaces_for,
@@ -1125,10 +1129,9 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1125
1129
  from latticeai.api.review_queue import create_review_queue_router
1126
1130
  run_review_item = build_review_run_now_runner(PLATFORM, HTTPException)
1127
1131
 
1128
- BRAIN_NETWORK = register_tail_runtime_routers(
1129
- app=app,
1132
+ register_review_and_brain_tail_routers(
1133
+ app,
1130
1134
  create_review_queue_router=create_review_queue_router,
1131
- register_review_and_brain_tail_routers=register_review_and_brain_tail_routers,
1132
1135
  review_queue=REVIEW_QUEUE,
1133
1136
  require_user=require_user,
1134
1137
  gate_read=PLATFORM.gate_read,
@@ -1137,6 +1140,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1137
1140
  append_audit_event=append_audit_event,
1138
1141
  create_browser_router=create_browser_router,
1139
1142
  ingestion_pipeline=INGESTION_PIPELINE,
1143
+ workspace_service=WORKSPACE_SERVICE,
1140
1144
  create_portability_router=create_portability_router,
1141
1145
  kg_portability=KG_PORTABILITY,
1142
1146
  require_admin=require_admin,
@@ -1157,9 +1161,9 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1157
1161
  uvicorn.run(app, host=DEFAULT_HOST, port=DEFAULT_PORT, log_level="info")
1158
1162
 
1159
1163
  # ── Constructed-namespace export (consumed by AppRuntime) ────────────────
1160
- # Public runtime objects and selected legacy helpers remain available via
1161
- # ``server_app.__getattr__``. Internal assembly scratch values are filtered
1162
- # out so the compatibility namespace can keep shrinking toward DI.
1164
+ # The five stages are typed and the compatibility surface is enumerated;
1165
+ # assembly locals never escape the composition root.
1166
+ router_bundle = build_router_bundle(app, context)
1163
1167
  runtime_bundle = RuntimeBundle(
1164
1168
  app=app,
1165
1169
  CONFIG=CONFIG,
@@ -1173,8 +1177,62 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
1173
1177
  build_runtime=build_runtime,
1174
1178
  get_shared_runtime=get_shared_runtime,
1175
1179
  create_app=create_app,
1180
+ config_runtime=_config_runtime,
1181
+ security_runtime=_security_runtime,
1182
+ brain_runtime=_brain_runtime,
1183
+ model_runtime=_model_runtime,
1184
+ router_bundle=router_bundle,
1185
+ )
1186
+ return build_runtime_namespace(
1187
+ runtime_bundle=runtime_bundle,
1188
+ legacy_exports={
1189
+ "ENGINE_MODEL_CATALOG": ENGINE_MODEL_CATALOG,
1190
+ "TOOL_GOVERNANCE": TOOL_GOVERNANCE,
1191
+ "enforce_rate_limit": enforce_rate_limit,
1192
+ "filter_lower_family_versions": filter_lower_family_versions,
1193
+ "hash_password": hash_password,
1194
+ "normalize_local_model_request": normalize_local_model_request,
1195
+ "verify_password": verify_password,
1196
+ "_LOCAL_WRITE_BLOCKED_PREFIXES": _LOCAL_WRITE_BLOCKED_PREFIXES,
1197
+ "_RATE_LIMIT_ENABLED": _RATE_LIMIT_ENABLED,
1198
+ "_SESSION_TTL": _SESSION_TTL,
1199
+ "_TOOL_CATALOG_BRIEF": _TOOL_CATALOG_BRIEF,
1200
+ "_TOOL_GOVERNANCE_DEFAULT": _TOOL_GOVERNANCE_DEFAULT,
1201
+ "_agent_risk": _agent_risk,
1202
+ "_allowed_workspaces_for": _allowed_workspaces_for,
1203
+ "_build_admin_audit_report": _build_admin_audit_report,
1204
+ "_build_sensitivity_report": _build_sensitivity_report,
1205
+ "_bytes_match_extension": _bytes_match_extension,
1206
+ "_check_ip_rate_limit": _check_ip_rate_limit,
1207
+ "_check_rate_limit": _check_rate_limit,
1208
+ "_check_tool_role": _check_tool_role,
1209
+ "_classify_sensitive_message": _classify_sensitive_message,
1210
+ "_client_ip": _client_ip,
1211
+ "_create_security_router": _create_security_router,
1212
+ "_embedding_info": _embedding_info,
1213
+ "_fetch_skills_marketplace": _fetch_skills_marketplace,
1214
+ "_get_audit_log": _get_audit_log,
1215
+ "_get_sso_discovery": _get_sso_discovery,
1216
+ "_graph_stats_safe": _graph_stats_safe,
1217
+ "_host_is_loopback": _host_is_loopback,
1218
+ "_list_compat_profiles": _list_compat_profiles,
1219
+ "_llm_generate_sync": _llm_generate_sync,
1220
+ "_product_hardening_status": _product_hardening_status,
1221
+ "_recent_chat_context": _recent_chat_context,
1222
+ "_redact_secret_text": _redact_secret_text,
1223
+ "_require_graph": _require_graph,
1224
+ "_scoped_hybrid_search": _scoped_hybrid_search,
1225
+ "_security_audit_events_safe": _security_audit_events_safe,
1226
+ "_security_list_uploaded_files": _security_list_uploaded_files,
1227
+ "_spawn": _spawn,
1228
+ "_tool_response": _tool_response,
1229
+ "_user_id_for_email": _user_id_for_email,
1230
+ "_workspace_graph": _workspace_graph,
1231
+ "_workspace_models_payload": _workspace_models_payload,
1232
+ "_workspace_scope_from_request": _workspace_scope_from_request,
1233
+ "_workspace_settings_payload": _workspace_settings_payload,
1234
+ },
1176
1235
  )
1177
- return build_runtime_namespace(locals(), runtime_bundle=runtime_bundle)
1178
1236
 
1179
1237
 
1180
1238
  @dataclass(frozen=True)
@@ -30,7 +30,8 @@ from typing import Any, Awaitable, Callable, Dict, FrozenSet, List, Optional
30
30
 
31
31
  from lattice_brain.runtime.hooks import dispatch_tool
32
32
  from lattice_brain.runtime.contracts import runtime_boundary_contract, single_agent_contract
33
- from tools import ToolError
33
+ from latticeai.core.tool_registry import SCOPED_KNOWLEDGE_TOOLS
34
+ from latticeai.tools import ToolError
34
35
 
35
36
 
36
37
  class AgentState(str, Enum):
@@ -68,9 +69,16 @@ class AgentRunContext:
68
69
  self.approved_by_human: bool = False
69
70
 
70
71
 
72
+ _THINK_BLOCK_RE = re.compile(
73
+ r"<(think|thinking|reasoning)>.*?</\1>", flags=re.DOTALL | re.IGNORECASE
74
+ )
75
+
76
+
71
77
  def extract_action(raw: str) -> Dict:
72
78
  """Parse one JSON action object out of an LLM response (tolerant of fences/prose)."""
73
- text = raw.strip()
79
+ # Small local models often prepend <think>...</think> reasoning that can
80
+ # itself contain braces — drop it before locating the action object.
81
+ text = _THINK_BLOCK_RE.sub("", raw).strip()
74
82
  fenced = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", text, flags=re.DOTALL)
75
83
  if fenced:
76
84
  text = fenced.group(1).strip()
@@ -82,8 +90,14 @@ def extract_action(raw: str) -> Dict:
82
90
 
83
91
  try:
84
92
  action = json.loads(text)
85
- except json.JSONDecodeError as exc:
86
- raise ValueError(f"Agent did not return valid JSON: {exc}") from exc
93
+ except json.JSONDecodeError:
94
+ # Second chance for the most common small-model JSON slips: trailing
95
+ # commas before a closing brace/bracket.
96
+ repaired = re.sub(r",\s*([}\]])", r"\1", text)
97
+ try:
98
+ action = json.loads(repaired)
99
+ except json.JSONDecodeError as exc:
100
+ raise ValueError(f"Agent did not return valid JSON: {exc}") from exc
87
101
 
88
102
  if not isinstance(action, dict) or "action" not in action:
89
103
  raise ValueError("Agent JSON must include an action field.")
@@ -155,7 +169,7 @@ class SingleAgentRuntime:
155
169
  entrypoint="latticeai.core.agent.SingleAgentRuntime",
156
170
  surface="/agent",
157
171
  owns="single-agent PLAN / EXECUTE / VERIFY state machine over injected ports",
158
- compatibility_aliases=["latticeai.core.agent.AgentRuntime"],
172
+ compatibility_aliases=[],
159
173
  )
160
174
 
161
175
  def config(self) -> Dict[str, Any]:
@@ -247,6 +261,7 @@ class SingleAgentRuntime:
247
261
  d = self.deps
248
262
  exec_count = sum(1 for s in ctx.transcript if s.get("state") == AgentState.EXECUTING.value)
249
263
  budget = max(1, max_steps - exec_count)
264
+ parse_failures = 0
250
265
 
251
266
  for _ in range(budget):
252
267
  corrections_hint = (
@@ -254,12 +269,20 @@ class SingleAgentRuntime:
254
269
  + "\n".join(f"- {c}" for c in ctx.corrections)
255
270
  ) if ctx.corrections else ""
256
271
 
272
+ request_workspace = getattr(req, "workspace_id", None)
273
+ recent_kwargs = {
274
+ "conversation_id": req.conversation_id,
275
+ "user_email": current_user or None,
276
+ }
277
+ if request_workspace is not None:
278
+ recent_kwargs["workspace_id"] = request_workspace
279
+ recent_conversation = d.recent_chat_context(**recent_kwargs) or "(none)"
257
280
  context = (
258
281
  f"{d.executor_prompt}\n\n"
259
282
  f"[LANGUAGE HINT: {lang_hint}]\n"
260
283
  f"Workspace root: {d.agent_root}\n\n"
261
284
  f"PLAN:\n{json.dumps(ctx.plan, ensure_ascii=False)}\n\n"
262
- f"Recent conversation:\n{d.recent_chat_context(conversation_id=req.conversation_id) or '(none)'}\n\n"
285
+ f"Recent conversation:\n{recent_conversation}\n\n"
263
286
  f"User request: {req.message}{corrections_hint}\n\n"
264
287
  f"Execution transcript:\n{json.dumps(ctx.transcript, ensure_ascii=False, indent=2)}"
265
288
  )
@@ -271,16 +294,36 @@ class SingleAgentRuntime:
271
294
  try:
272
295
  action = extract_action(str(raw))
273
296
  except ValueError as exc:
297
+ parse_failures += 1
274
298
  ctx.transcript.append({
275
299
  "state": AgentState.EXECUTING.value, "action": "parse_error",
276
300
  "raw": str(raw)[:400], "error": str(exc),
277
301
  })
278
- break
302
+ if parse_failures >= 3:
303
+ break
304
+ # Weak models often need one concrete reminder of the wire
305
+ # format; feed it through the corrections channel and retry
306
+ # instead of aborting the whole run on the first slip.
307
+ hint = (
308
+ 'Your last reply was not a single JSON action object. Reply with '
309
+ 'EXACTLY one JSON object like {"thoughts": "...", "action": '
310
+ '"tool_name", "args": {...}} and nothing else.'
311
+ )
312
+ if hint not in ctx.corrections:
313
+ ctx.corrections.append(hint)
314
+ continue
279
315
 
280
316
  name = action.get("action")
281
317
  thoughts = str(action.get("thoughts") or "")[:600]
282
318
  args = action.get("args") or {}
283
319
 
320
+ if name in SCOPED_KNOWLEDGE_TOOLS:
321
+ # Scope is server-owned, never model-owned. Overwrite any
322
+ # claimed values before policy evaluation, audit, and dispatch.
323
+ args = dict(args)
324
+ args["workspace_id"] = request_workspace or "personal"
325
+ args["user_email"] = current_user or "local"
326
+
284
327
  if name == "final":
285
328
  ctx.final_message = action.get("message", "작업을 완료했습니다.")
286
329
  ctx.transcript.append({
@@ -530,9 +573,3 @@ class SingleAgentRuntime:
530
573
  ctx.state = AgentState.FAILED
531
574
 
532
575
  ctx.state_history.append(ctx.state.value)
533
-
534
-
535
- # Backward compatibility: external callers historically imported
536
- # ``latticeai.core.agent.AgentRuntime`` for the single-agent state machine.
537
- # The product/runtime facade lives at ``lattice_brain.runtime.agent_runtime``.
538
- AgentRuntime = SingleAgentRuntime
@@ -43,6 +43,11 @@ You think and act like a senior software engineer:
43
43
  Respond with exactly ONE JSON object per step:
44
44
  {"thoughts": "what you learned / why this next action", "action": "tool_name", "args": {...}}
45
45
 
46
+ When writing a file (write_file), args.content must be the COMPLETE raw file
47
+ content: no Markdown fences, no commentary, valid for the file's extension
48
+ (an .html file starts with <!DOCTYPE html> and ends with </html>; a .json
49
+ file must parse as strict JSON).
50
+
46
51
  When the task is fully done AND a tool result in this run confirms it:
47
52
  {"thoughts": "verified", "action": "final", "message": "한국어로 무엇을 했고 어디서 검증했는지 요약"}
48
53
 
@@ -18,7 +18,6 @@ from __future__ import annotations
18
18
  import json
19
19
  import os
20
20
  import tempfile
21
- from datetime import datetime
22
21
  from pathlib import Path
23
22
  from typing import Any, Dict, List, Optional
24
23
 
@@ -28,14 +27,11 @@ from lattice_brain.runtime.multi_agent import (
28
27
  MULTI_AGENT_VERSION,
29
28
  ROLE_AGENT_IDS,
30
29
  )
30
+ from .timeutil import now_iso as _now
31
31
 
32
32
  AGENT_TYPES = ("planner", "researcher", "executor", "reviewer", "release", "custom")
33
33
 
34
34
 
35
- def _now() -> str:
36
- return datetime.now().isoformat(timespec="seconds")
37
-
38
-
39
35
  # Capabilities + descriptions for the built-in role agents. Kept here as the
40
36
  # registry's metadata projection of the roles defined in multi_agent.py.
41
37
  ROLE_META: Dict[str, Dict[str, Any]] = {
@@ -99,6 +99,7 @@ class Config:
99
99
  rate_limit_enabled: bool
100
100
  open_registration: bool
101
101
  invite_code: str
102
+ invite_cookie_secret: str
102
103
  invite_gate_enabled: bool
103
104
  admin_emails: List[str]
104
105
  trusted_proxies: List[str]
@@ -160,6 +161,7 @@ class Config:
160
161
  host = _value(env, "LATTICEAI_HOST", "127.0.0.1")
161
162
  port = _port(env, "LATTICEAI_PORT", 4825)
162
163
  network_exposed = not host_is_loopback(host)
164
+ externally_reachable = is_public or network_exposed
163
165
 
164
166
  cors_extra = [item.strip() for item in _value(env, "LATTICEAI_CORS_ALLOWED_ORIGINS", "").split(",") if item.strip()]
165
167
  admin_emails = [item.strip().lower() for item in _value(env, "LATTICEAI_ADMIN_EMAILS", "").split(",") if item.strip()]
@@ -189,13 +191,31 @@ class Config:
189
191
  autoload_models=_bool(env, "LATTICEAI_AUTOLOAD_MODELS", default=is_public),
190
192
  model_idle_unload_seconds=_int(env, "LATTICEAI_MODEL_IDLE_UNLOAD_SECONDS", 0),
191
193
  allow_local_models=_bool(env, "LATTICEAI_ALLOW_LOCAL_MODELS", default=not is_public),
192
- require_auth=_bool(env, "LATTICEAI_REQUIRE_AUTH", default=is_public or network_exposed),
194
+ # Authentication is optional only for the local-first loopback
195
+ # profile. An explicit ``false`` must never turn a public/LAN
196
+ # binding into an unauthenticated service.
197
+ require_auth=(
198
+ True
199
+ if externally_reachable
200
+ else _bool(env, "LATTICEAI_REQUIRE_AUTH", default=False)
201
+ ),
193
202
  allow_plaintext_api_keys=_bool(env, "LATTICEAI_ALLOW_PLAINTEXT_API_KEYS", default=False),
194
203
  cors_allow_network=_bool(env, "LATTICEAI_CORS_ALLOW_NETWORK", default=False),
195
204
  cors_extra_origins=cors_extra,
196
205
  rate_limit_enabled=_str(env, "LATTICEAI_RATE_LIMIT", "1") != "0",
197
- open_registration=_bool(env, "LATTICEAI_OPEN_REGISTRATION", default=not network_exposed and not is_public),
198
- invite_code=_value(env, "LATTICEAI_INVITE_CODE", "gemma-lattice-ai"),
206
+ # Public/LAN startup is closed-registration even if a stale or
207
+ # unsafe environment file attempts to opt back in.
208
+ open_registration=(
209
+ False
210
+ if externally_reachable
211
+ else _bool(env, "LATTICEAI_OPEN_REGISTRATION", default=True)
212
+ ),
213
+ # There is deliberately no repository-wide/default invitation
214
+ # code. When the gate is enabled, the security runtime persists a
215
+ # cryptographically random code if the operator did not provide
216
+ # one explicitly.
217
+ invite_code=_value(env, "LATTICEAI_INVITE_CODE", ""),
218
+ invite_cookie_secret=_value(env, "LATTICEAI_INVITE_COOKIE_SECRET", ""),
199
219
  invite_gate_enabled=_bool(env, "LATTICEAI_INVITE_GATE_ENABLED", default=False),
200
220
  admin_emails=admin_emails,
201
221
  trusted_proxies=trusted_proxies,