ltcai 10.2.0 → 10.4.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 (149) hide show
  1. package/README.md +40 -44
  2. package/docs/CHANGELOG.md +125 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/MYPY_BACKLOG.md +65 -0
  6. package/docs/ONBOARDING.md +1 -1
  7. package/docs/OPERATIONS.md +1 -1
  8. package/docs/SURFACE_PARITY.md +33 -6
  9. package/docs/TRUST_MODEL.md +1 -1
  10. package/docs/WHY_LATTICE.md +1 -1
  11. package/docs/kg-schema.md +1 -1
  12. package/lattice_brain/__init__.py +1 -1
  13. package/lattice_brain/context.py +23 -3
  14. package/lattice_brain/graph/_kg_common.py +164 -9
  15. package/lattice_brain/graph/_kg_contract.py +223 -0
  16. package/lattice_brain/graph/_kg_fsutil.py +2 -2
  17. package/lattice_brain/graph/curator.py +4 -4
  18. package/lattice_brain/graph/discovery.py +12 -1
  19. package/lattice_brain/graph/discovery_index.py +12 -1
  20. package/lattice_brain/graph/documents.py +17 -2
  21. package/lattice_brain/graph/ingest.py +12 -1
  22. package/lattice_brain/graph/proactive.py +9 -5
  23. package/lattice_brain/graph/projection.py +12 -1
  24. package/lattice_brain/graph/provenance.py +12 -1
  25. package/lattice_brain/graph/retrieval.py +12 -1
  26. package/lattice_brain/graph/retrieval_docgen.py +12 -1
  27. package/lattice_brain/graph/retrieval_reads.py +12 -1
  28. package/lattice_brain/graph/retrieval_vector.py +12 -1
  29. package/lattice_brain/graph/schema.py +6 -2
  30. package/lattice_brain/graph/write_master.py +12 -1
  31. package/lattice_brain/ingestion.py +1 -1
  32. package/lattice_brain/quality.py +5 -5
  33. package/lattice_brain/runtime/agent_runtime.py +7 -2
  34. package/lattice_brain/runtime/contracts.py +2 -1
  35. package/lattice_brain/runtime/hooks.py +1 -1
  36. package/lattice_brain/runtime/multi_agent.py +1 -1
  37. package/lattice_brain/workflow.py +10 -7
  38. package/latticeai/__init__.py +1 -1
  39. package/latticeai/api/admin.py +2 -2
  40. package/latticeai/api/chat.py +19 -15
  41. package/latticeai/api/chat_agent_http.py +19 -7
  42. package/latticeai/api/chat_helpers.py +9 -8
  43. package/latticeai/api/chat_stream.py +2 -2
  44. package/latticeai/api/computer_use.py +2 -2
  45. package/latticeai/api/hooks.py +4 -2
  46. package/latticeai/api/local_files.py +8 -4
  47. package/latticeai/api/mcp.py +2 -2
  48. package/latticeai/api/models.py +5 -5
  49. package/latticeai/api/permissions.py +2 -2
  50. package/latticeai/api/search.py +5 -3
  51. package/latticeai/api/security_dashboard.py +3 -5
  52. package/latticeai/api/setup.py +3 -3
  53. package/latticeai/api/static_routes.py +7 -2
  54. package/latticeai/api/tools.py +9 -4
  55. package/latticeai/api/workspace.py +25 -22
  56. package/latticeai/app_factory.py +132 -1346
  57. package/latticeai/cli/entrypoint.py +1 -1
  58. package/latticeai/core/agent.py +28 -14
  59. package/latticeai/core/audit.py +5 -3
  60. package/latticeai/core/embedding_providers.py +19 -6
  61. package/latticeai/core/legacy_compatibility.py +1 -1
  62. package/latticeai/core/marketplace.py +2 -2
  63. package/latticeai/core/mcp_catalog.py +3 -1
  64. package/latticeai/core/model_compat.py +1 -1
  65. package/latticeai/core/plugins.py +1 -1
  66. package/latticeai/core/project_sessions.py +1 -1
  67. package/latticeai/core/run_explain.py +6 -4
  68. package/latticeai/core/workspace_os.py +0 -4
  69. package/latticeai/core/workspace_os_constants.py +1 -1
  70. package/latticeai/core/workspace_os_state.py +1 -1
  71. package/latticeai/core/workspace_snapshots.py +3 -3
  72. package/latticeai/integrations/telegram_bot.py +52 -3
  73. package/latticeai/models/router.py +61 -34
  74. package/latticeai/runtime/access_runtime.py +3 -1
  75. package/latticeai/runtime/build_phases.py +1417 -0
  76. package/latticeai/runtime/history_writer.py +138 -0
  77. package/latticeai/runtime/review_wiring.py +6 -1
  78. package/latticeai/runtime/runtime_context.py +276 -0
  79. package/latticeai/runtime/sso_config_runtime.py +1 -1
  80. package/latticeai/runtime/stages.py +8 -2
  81. package/latticeai/services/app_context.py +23 -0
  82. package/latticeai/services/architecture_readiness.py +1 -1
  83. package/latticeai/services/brain_intelligence.py +4 -2
  84. package/latticeai/services/cloud_streaming.py +5 -3
  85. package/latticeai/services/command_center.py +1 -1
  86. package/latticeai/services/evidence_actions.py +2 -1
  87. package/latticeai/services/hybrid_chat.py +1 -1
  88. package/latticeai/services/local_knowledge.py +25 -9
  89. package/latticeai/services/memory_service.py +8 -4
  90. package/latticeai/services/model_catalog.py +12 -12
  91. package/latticeai/services/model_engines.py +15 -17
  92. package/latticeai/services/model_loading.py +1 -1
  93. package/latticeai/services/model_runtime.py +31 -33
  94. package/latticeai/services/multimodal_streaming.py +10 -4
  95. package/latticeai/services/openai_compatible_adapter.py +4 -3
  96. package/latticeai/services/product_readiness.py +1 -1
  97. package/latticeai/services/run_executor.py +12 -3
  98. package/latticeai/services/search_service.py +1 -1
  99. package/latticeai/services/triggers.py +1 -1
  100. package/latticeai/setup/auto_setup.py +1 -1
  101. package/latticeai/setup/wizard.py +12 -3
  102. package/latticeai/tools/computer.py +3 -1
  103. package/latticeai/tools/documents.py +2 -2
  104. package/package.json +4 -2
  105. package/scripts/check_current_release_docs.mjs +1 -1
  106. package/src-tauri/Cargo.lock +1 -1
  107. package/src-tauri/Cargo.toml +1 -1
  108. package/src-tauri/tauri.conf.json +1 -1
  109. package/static/app/asset-manifest.json +37 -37
  110. package/static/app/assets/{Act-CbdGD-2i.js → Act-BK__Y5VE.js} +1 -1
  111. package/static/app/assets/{AdminConsole-LgCkXpnf.js → AdminConsole-ZrTyN8_F.js} +1 -1
  112. package/static/app/assets/{Brain-CoPGJI1L.js → Brain-CYHDXpPB.js} +1 -1
  113. package/static/app/assets/{BrainHome-gVnaxSwp.js → BrainHome-bbJulb5g.js} +1 -1
  114. package/static/app/assets/{BrainSignals-ChxAYHtj.js → BrainSignals-CesSPtUP.js} +1 -1
  115. package/static/app/assets/{Capture-DBtgkHZg.js → Capture-p5upXmz2.js} +1 -1
  116. package/static/app/assets/{CommandPalette-Ovtv5I0Y.js → CommandPalette-0zkyC0RY.js} +1 -1
  117. package/static/app/assets/Library-Dt-vABI2.js +1 -0
  118. package/static/app/assets/{LivingBrain-CNz-6NSd.js → LivingBrain-Bz-h6LBH.js} +1 -1
  119. package/static/app/assets/{ProductFlow-ePX-Y73J.js → ProductFlow-DAZ7DJCE.js} +1 -1
  120. package/static/app/assets/{ReviewCard-C4mpvkwH.js → ReviewCard-Bv0WGbY6.js} +1 -1
  121. package/static/app/assets/{System-BvWNK1zc.js → System-BhrJQSD_.js} +1 -1
  122. package/static/app/assets/{activity-CiauPV_3.js → activity-AolpZJCH.js} +1 -1
  123. package/static/app/assets/{bot-Dwct-Q1x.js → bot-CsVsT2nd.js} +1 -1
  124. package/static/app/assets/{brain-BKDW1F0T.js → brain-vuppco5y.js} +1 -1
  125. package/static/app/assets/{button-CCB9Kuw7.js → button-1BP1PJZ3.js} +1 -1
  126. package/static/app/assets/{circle-pause-DNa8WHC5.js → circle-pause-5paeCOtq.js} +1 -1
  127. package/static/app/assets/{circle-play-8jmxn5mf.js → circle-play-By8mFYRV.js} +1 -1
  128. package/static/app/assets/{cpu-mxvF3V1I.js → cpu-DvUQxdKe.js} +1 -1
  129. package/static/app/assets/{download--SmCcx0p.js → download-CFqYG90a.js} +1 -1
  130. package/static/app/assets/{folder-open-D4Wy5roo.js → folder-open-CVSnp-Dp.js} +1 -1
  131. package/static/app/assets/{hard-drive-BwQcSPlS.js → hard-drive-Ckr4Vd6-.js} +1 -1
  132. package/static/app/assets/{index-DDV2YZwM.js → index--0zKF5Ow.js} +3 -3
  133. package/static/app/assets/index-CEkL4_M-.css +2 -0
  134. package/static/app/assets/{input-DFhSjmLS.js → input-D-JKquXU.js} +1 -1
  135. package/static/app/assets/{network-Bts7VO94.js → network-DB88FZ2T.js} +1 -1
  136. package/static/app/assets/{primitives-BuSMEJY8.js → primitives-D8U9OaIo.js} +1 -1
  137. package/static/app/assets/search-B8jsVTw_.js +1 -0
  138. package/static/app/assets/{shield-alert-BfTO6X_G.js → shield-alert-DnsvO79P.js} +1 -1
  139. package/static/app/assets/{textarea-BjctW1oh.js → textarea-4RchwbTa.js} +1 -1
  140. package/static/app/assets/{useFocusTrap-CE43-LrA.js → useFocusTrap-CFJjy7cS.js} +1 -1
  141. package/static/app/assets/{useQuery-Bv3ejLL5.js → useQuery-3Reu-A4a.js} +1 -1
  142. package/static/app/assets/{users-CsNqLZAj.js → users-BTxYksxa.js} +1 -1
  143. package/static/app/assets/{utils-KFFdVG_t.js → utils-D02Vf-fw.js} +1 -1
  144. package/static/app/assets/{workspace-wdCvdyPF.js → workspace-CSJNgX-C.js} +1 -1
  145. package/static/app/index.html +4 -4
  146. package/static/sw.js +1 -1
  147. package/static/app/assets/Library-CBia2Fvm.js +0 -1
  148. package/static/app/assets/index-CQWdDU3z.css +0 -2
  149. package/static/app/assets/search-DZzxhWaQ.js +0 -1
@@ -0,0 +1,138 @@
1
+ """Persisting one chat turn: redact, audit, store, then grow the Brain.
2
+
3
+ Extracted from the `app_factory._build` closure in 10.3.0. It was 66 lines
4
+ deep inside a 1,343-line composition root, which meant the one function that
5
+ decides what a chat message looks like *after* redaction — and what the audit
6
+ log records about it — could not be exercised without standing up the whole
7
+ application.
8
+
9
+ The order here is the contract, and it is deliberate:
10
+
11
+ 1. **Redact first.** Everything downstream (the audit preview, the durable
12
+ store, the knowledge graph) sees the redacted text, never the original.
13
+ 2. **Audit before storing.** The audit row carries a masked preview and the
14
+ sensitivity verdict, so a message that should not have been sent is
15
+ visible in the log even if the store write later fails.
16
+ 3. **Ingest through the pipeline, not the store.** Chat messages enter the
17
+ Brain the same way files and web pages do, so they get provenance and the
18
+ hook lifecycle rather than a direct write that bypasses both.
19
+ 4. **A failed ingest does not lose the message.** Graph growth is best-effort;
20
+ the conversation store is not.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import logging
26
+ from dataclasses import dataclass
27
+ from datetime import datetime
28
+ from typing import Any, Callable, Dict, Optional
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class HistoryWriterDeps:
35
+ """Everything one chat turn needs, injected so this is testable in isolation."""
36
+
37
+ conversations: Any
38
+ append_audit_event: Callable[..., None]
39
+ classify_sensitive_message: Callable[[Dict[str, Any], int], Dict[str, Any]]
40
+ redact_secret_text: Callable[[str], str]
41
+ normalize_branding: Callable[[str], str]
42
+ ingestion_pipeline: Any = None
43
+ ingestion_item_factory: Any = None
44
+ enable_graph: bool = False
45
+ knowledge_graph: Any = None
46
+
47
+
48
+ def write_chat_turn(
49
+ role: str,
50
+ message: str,
51
+ *,
52
+ user_email: Optional[str] = None,
53
+ user_nickname: Optional[str] = None,
54
+ source: Optional[str] = None,
55
+ conversation_id: Optional[str] = None,
56
+ workspace_id: Optional[str] = None,
57
+ deps: HistoryWriterDeps,
58
+ ) -> None:
59
+ """Persist one chat turn. Never raises: a failure here must not lose a reply."""
60
+ redact_secret_text = deps.redact_secret_text
61
+ normalize_branding = deps.normalize_branding
62
+ classify_sensitive_message = deps.classify_sensitive_message
63
+ append_audit_event = deps.append_audit_event
64
+ conversations = deps.conversations
65
+ ingestion_pipeline = deps.ingestion_pipeline
66
+ ingestion_item_factory = deps.ingestion_item_factory
67
+ enable_graph = deps.enable_graph
68
+ knowledge_graph = deps.knowledge_graph
69
+
70
+ try:
71
+ # 1. Redact before anything else sees the text.
72
+ message = redact_secret_text(message)
73
+ if role == "assistant":
74
+ message = normalize_branding(message)
75
+
76
+ item: Dict[str, Any] = {
77
+ "role": role,
78
+ "content": message,
79
+ "timestamp": datetime.now().isoformat(),
80
+ }
81
+ for key, value in (
82
+ ("user_email", user_email),
83
+ ("user_nickname", user_nickname),
84
+ ("source", source),
85
+ ("conversation_id", conversation_id),
86
+ ("workspace_id", workspace_id),
87
+ ):
88
+ if value:
89
+ item[key] = value
90
+
91
+ # 2. Audit carries a masked preview and the verdict, never the body.
92
+ sensitive = classify_sensitive_message(item, -1)
93
+ append_audit_event(
94
+ "chat_message",
95
+ role=role,
96
+ user_email=user_email,
97
+ user_nickname=user_nickname,
98
+ source=source,
99
+ conversation_id=conversation_id,
100
+ workspace_id=workspace_id,
101
+ content_preview=sensitive.get("preview"),
102
+ content_chars=len(message or ""),
103
+ sensitivity=sensitive.get("sensitivity"),
104
+ sensitive_labels=sensitive.get("labels") or [],
105
+ )
106
+
107
+ # 3. Durable episodic memory. Unbounded SQLite; the old 50-message
108
+ # chat_history.json cap is gone.
109
+ conversations.append(item)
110
+
111
+ # 4. Best-effort Brain growth through the unified pipeline.
112
+ if enable_graph and knowledge_graph and ingestion_pipeline and ingestion_item_factory:
113
+ try:
114
+ ingestion_pipeline.ingest(
115
+ ingestion_item_factory(
116
+ source_type="chat_message",
117
+ text=message,
118
+ owner=user_email,
119
+ workspace_id=workspace_id,
120
+ conversation_id=conversation_id,
121
+ metadata={
122
+ "role": role,
123
+ "user_nickname": user_nickname,
124
+ "source": source,
125
+ "raw": item,
126
+ },
127
+ ),
128
+ user_email=user_email,
129
+ )
130
+ except Exception as graph_error: # noqa: BLE001
131
+ # The message is already stored; a graph failure must not
132
+ # look like a lost turn.
133
+ logger.warning("knowledge graph message ingest failed: %s", graph_error)
134
+ except Exception as exc: # noqa: BLE001
135
+ logger.warning("save_to_history failed: %s", exc)
136
+
137
+
138
+ __all__ = ["HistoryWriterDeps", "write_chat_turn"]
@@ -5,7 +5,12 @@ from __future__ import annotations
5
5
  from typing import Any, Callable, Dict, Optional
6
6
 
7
7
 
8
- def build_review_run_now_runner(platform: Any, http_exception: type[Exception]) -> Callable[..., Any]:
8
+ def build_review_run_now_runner(
9
+ platform: Any,
10
+ # Injected as `fastapi.HTTPException`, which takes (status_code, detail) —
11
+ # not the bare `Exception` signature the narrower annotation implied.
12
+ http_exception: Callable[..., Exception],
13
+ ) -> Callable[..., Any]:
9
14
  """Build the Review Center run-now runner used by the API router.
10
15
 
11
16
  The runner preserves the public contract: "Run now" previews/regenerates the
@@ -0,0 +1,276 @@
1
+ """The assembly state the build phases share.
2
+
3
+ ``app_factory._build`` was one 1,300-line function. That was not an accident:
4
+ its sections genuinely depend on each other, and several of them close over
5
+ names bound *further down* — ``save_to_history`` calls ``append_audit_event``,
6
+ which is built ~150 lines later. Inside a single function that works, because a
7
+ closure resolves a free variable when it is called, not when it is defined.
8
+ Any naive extraction breaks exactly there (10.3.0 hit this and documented it).
9
+
10
+ ``RuntimeContext`` is what makes the extraction safe. It is a mutable object
11
+ that every phase writes into and reads from, so a closure created in phase 2
12
+ that calls ``ctx.append_audit_event`` still resolves it at call time — the same
13
+ late binding the single function had, now with the shared state named instead
14
+ of implicit.
15
+
16
+ Two properties keep it honest:
17
+
18
+ * **Attributes are declared, not defaulted.** Reading one before its phase has
19
+ run raises ``AttributeError`` naming the attribute, rather than silently
20
+ yielding ``None`` and failing somewhere unrelated.
21
+ * **The phase order is a contract.** ``tests/unit/test_runtime_context.py``
22
+ fixes which phase produces which attribute, so reordering the phases in a way
23
+ that breaks a dependency fails a test instead of production.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ from typing import Any, Dict, List, Set
29
+
30
+
31
+ class RuntimeContext:
32
+ """Mutable state carried across the ordered application build phases."""
33
+
34
+ # ── phase 1: platform ────────────────────────────────────────────────
35
+ mx: Any
36
+
37
+ # ── phase 2: configuration and paths ─────────────────────────────────
38
+ config_arg: Any
39
+ config_runtime: Any
40
+ security_runtime: Any
41
+ CONFIG: Any
42
+ APP_VERSION: str
43
+ APP_MODE: str
44
+ IS_PUBLIC_MODE: bool
45
+ DEFAULT_HOST: str
46
+ DEFAULT_PORT: int
47
+ ENABLE_TELEGRAM: bool
48
+ ENABLE_GRAPH: bool
49
+ AUTOLOAD_MODELS: bool
50
+ MODEL_IDLE_UNLOAD_SECONDS: Any
51
+ ALLOW_MODEL_DOWNLOADS: bool
52
+ MODEL_DOWNLOAD_TIMEOUT: Any
53
+ ALLOW_LOCAL_MODELS: bool
54
+ REQUIRE_AUTH: bool
55
+ ALLOW_PLAINTEXT_API_KEYS: bool
56
+ CORS_ALLOW_NETWORK: bool
57
+ CORS_EXTRA_ORIGINS: Any
58
+ PUBLIC_MODEL: str
59
+ LOCAL_MODEL: Any
60
+ LOCAL_DRAFT_MODEL: Any
61
+ OPEN_REGISTRATION: bool
62
+ SSO_DISCOVERY_URL: Any
63
+ SSO_CLIENT_ID: Any
64
+ SSO_CLIENT_SECRET: Any
65
+ SSO_REDIRECT_URI: Any
66
+ SSO_PROVIDER_NAME: Any
67
+ INVITE_CODE: Any
68
+ INVITE_COOKIE_SECRET: Any
69
+ INVITE_GATE_ENABLED: bool
70
+ SECURE_COOKIES: bool
71
+ BASE_DIR: Any
72
+ DATA_DIR: Any
73
+ STATIC_DIR: Any
74
+ USERS_FILE: Any
75
+ HISTORY_FILE: Any
76
+ VPC_FILE: Any
77
+ AUDIT_FILE: Any
78
+ SSO_FILE: Any
79
+ keyring: Any
80
+
81
+ # ── phase 3: identity, audit, history ────────────────────────────────
82
+ load_mcp_installs: Any
83
+ mcp_public_item: Any
84
+ recommend_mcps: Any
85
+ install_mcp: Any
86
+ _SESSION_TTL: Any
87
+ _session_store: Any
88
+ create_session: Any
89
+ get_session_email: Any
90
+ invalidate_session: Any
91
+ load_users: Any
92
+ save_users: Any
93
+ user_id_for_email: Any
94
+ verify_and_migrate_password: Any
95
+ redact_secret_text: Any
96
+ get_audit_log: Any
97
+ append_audit_event: Any
98
+ classify_sensitive_message: Any
99
+ build_sensitivity_report: Any
100
+ build_admin_audit_report: Any
101
+ get_user_role: Any
102
+ _extract_bearer_token: Any
103
+ get_current_user: Any
104
+ require_user: Any
105
+ require_admin: Any
106
+ public_user: Any
107
+ _RATE_LIMIT_ENABLED: bool
108
+ enforce_rate_limit: Any
109
+ _check_rate_limit: Any
110
+ _client_ip: Any
111
+ _bytes_match_extension: Any
112
+ _host_is_loopback: Any
113
+ get_history_user: Any
114
+ get_user_api_key: Any
115
+ set_user_api_key: Any
116
+ get_sso_settings: Any
117
+ public_sso_config: Any
118
+ save_sso_config: Any
119
+ _get_sso_discovery: Any
120
+ _sso_states: Any
121
+ _sso_env_defaults: Any
122
+ load_vpc_config: Any
123
+ save_vpc_config: Any
124
+
125
+ # ── phase 4: brain, persistence ──────────────────────────────────────
126
+ EMBEDDING_PROFILE: Any
127
+ EMBEDDER: Any
128
+ STORAGE_ENGINE: Any
129
+ brain_runtime: Any
130
+ KNOWLEDGE_GRAPH: Any
131
+ CONVERSATIONS: Any
132
+ save_to_history: Any
133
+ HOOKS_REGISTRY: Any
134
+ LOCAL_KG_WATCHER: Any
135
+ REALTIME_BUS: Any
136
+ WORKSPACE_OS: Any
137
+ WORKSPACE_SERVICE: Any
138
+ INVITATION_STORE: Any
139
+ PLUGIN_REGISTRY: Any
140
+ TEMPLATE_CATALOG: Any
141
+ AGENT_REGISTRY: Any
142
+ MEMORY_SERVICE: Any
143
+ BRAIN_INTELLIGENCE: Any
144
+ AUTOMATION_INTELLIGENCE: Any
145
+ INGESTION_PIPELINE: Any
146
+ DEVICE_IDENTITY: Any
147
+ KG_PORTABILITY: Any
148
+ FUNNEL_METRICS: Any
149
+ get_history: Any
150
+ conversation_title: Any
151
+ group_history_conversations: Any
152
+ get_conversation_messages: Any
153
+ clear_history: Any
154
+ clear_conversation: Any
155
+ _history_allowed_workspaces_for: Any
156
+ _history_include_legacy_global: Any
157
+ _require_graph: Any
158
+ _workspace_graph: Any
159
+
160
+ # ── phase 5: domain services ─────────────────────────────────────────
161
+ model_router: Any
162
+ gardener: Any
163
+ CHAT_SERVICE: Any
164
+ SEARCH_SERVICE: Any
165
+ BRAIN_MEMORY: Any
166
+ CONTEXT_ASSEMBLER: Any
167
+ ARTIFACT_LEDGER: Any
168
+ _scoped_hybrid_search: Any
169
+ CHAT_AGENT_RUNTIME: Any
170
+ on_chat_message: Any
171
+ _recent_chat_context: Any
172
+ _workspace_settings_payload: Any
173
+ _workspace_models_payload: Any
174
+ _embedding_info: Any
175
+ _allowed_workspaces_for: Any
176
+ app_context: Any
177
+
178
+ # ── phase 6: web app and foundation routers ──────────────────────────
179
+ _spawn: Any
180
+ lifespan: Any
181
+ app: Any
182
+ model_runtime_service: Any
183
+ STATIC_ROUTES: Any
184
+ ui_file_response: Any
185
+ local_sysinfo: Any
186
+ invite_authorized: Any
187
+ auth_router: Any
188
+ admin_router: Any
189
+ invitations_router: Any
190
+ security_router: Any
191
+ _graph_stats_safe: Any
192
+ _product_hardening_status: Any
193
+ _security_audit_events_safe: Any
194
+ _security_list_uploaded_files: Any
195
+
196
+ # ── phase 7: platform features ───────────────────────────────────────
197
+ _llm_generate_sync: Any
198
+ PLATFORM: Any
199
+ _automation_runtime: Any
200
+ REVIEW_QUEUE: Any
201
+ TRIGGER_SERVICE: Any
202
+ AGENT_RUNTIME: Any
203
+ RUN_EXECUTOR: Any
204
+ COMMAND_CENTER: Any
205
+ CHANGE_PROPOSALS: Any
206
+ EVIDENCE_ACTIONS_SERVICE: Any
207
+ VOICE_CAPTURE: Any
208
+ PROJECT_SESSIONS: Any
209
+
210
+ # ── phase 8: interaction routers ─────────────────────────────────────
211
+ model_runtime: Any
212
+
213
+ # ── legacy compatibility surface (phase 9 reads these) ───────────────
214
+ legacy: Dict[str, Any]
215
+
216
+ def __init__(self, config: Any = None) -> None:
217
+ self.config_arg = config
218
+ self.legacy = {}
219
+ self._produced: Dict[str, str] = {}
220
+ self._phase: str = "<none>"
221
+
222
+ # ── phase bookkeeping ────────────────────────────────────────────────
223
+ def enter(self, phase: str) -> None:
224
+ """Mark which phase subsequent ``set`` calls belong to."""
225
+ self._phase = phase
226
+
227
+ def set(self, **values: Any) -> None:
228
+ """Publish values produced by the current phase.
229
+
230
+ Recording the producing phase is what lets the ordering test assert a
231
+ contract instead of a comment.
232
+ """
233
+ for name, value in values.items():
234
+ setattr(self, name, value)
235
+ self._produced.setdefault(name, self._phase)
236
+
237
+ def adopt(self, stage: Any, *names: str) -> None:
238
+ """Publish selected keys of a runtime-stage mapping onto the context."""
239
+ self.set(**{name: stage[name] for name in names})
240
+
241
+ def require(self, name: str) -> Any:
242
+ """Read an attribute, failing with the phase contract if it is unset.
243
+
244
+ The bare ``AttributeError`` is already specific; this adds which phase
245
+ was supposed to have produced the value, which is the actual question
246
+ when a phase gets reordered.
247
+ """
248
+ try:
249
+ return getattr(self, name)
250
+ except AttributeError:
251
+ producer = self._produced.get(name)
252
+ hint = f" (produced by phase {producer!r})" if producer else ""
253
+ raise RuntimeError(
254
+ f"RuntimeContext.{name} was read before it was built{hint}"
255
+ ) from None
256
+
257
+ @property
258
+ def produced_by(self) -> Dict[str, str]:
259
+ """Attribute name → the phase that published it, for the order test."""
260
+ return dict(self._produced)
261
+
262
+ def names(self) -> Set[str]:
263
+ return set(self._produced)
264
+
265
+ def phases_run(self) -> List[str]:
266
+ seen: List[str] = []
267
+ for phase in self._produced.values():
268
+ if phase not in seen:
269
+ seen.append(phase)
270
+ return seen
271
+
272
+ def __repr__(self) -> str: # pragma: no cover - debugging aid
273
+ return f"<RuntimeContext phase={self._phase!r} names={len(self._produced)}>"
274
+
275
+
276
+ __all__ = ["RuntimeContext"]
@@ -97,7 +97,7 @@ def build_sso_config_runtime(
97
97
 
98
98
  async def _get_sso_discovery() -> Optional[Dict]:
99
99
  settings = get_sso_settings()
100
- url = settings.get("discovery_url", "")
100
+ url = str(settings.get("discovery_url", "") or "")
101
101
  if _discovery_cache["data"] and _discovery_cache["url"] == url:
102
102
  return _discovery_cache["data"]
103
103
  if not url:
@@ -2,13 +2,19 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from dataclasses import fields
6
- from typing import Any, Iterator, Mapping
5
+ from dataclasses import Field, fields
6
+ from typing import TYPE_CHECKING, Any, ClassVar, Dict, Iterator, Mapping
7
7
 
8
8
 
9
9
  class RuntimeStage(Mapping[str, Any]):
10
10
  """Dataclass mixin that preserves the legacy mapping access during DI migration."""
11
11
 
12
+ if TYPE_CHECKING:
13
+ # The mixin is only ever combined with @dataclass classes; declaring
14
+ # the marker attribute is how a type checker learns that `fields(self)`
15
+ # is legal here (see the dataclasses stubs' DataclassInstance protocol).
16
+ __dataclass_fields__: ClassVar[Dict[str, Field[Any]]]
17
+
12
18
  def __getitem__(self, key: str) -> Any:
13
19
  if key not in self:
14
20
  raise KeyError(key)
@@ -97,3 +97,26 @@ class AppContext:
97
97
  # persisted; ``None`` means no external chat mirror is registered. The
98
98
  # telegram bridge subscribes here only when ENABLE_TELEGRAM is truthy.
99
99
  on_chat_message: Optional[Callable[..., None]] = None
100
+
101
+ def require(self, field: str) -> Any:
102
+ """Return a dependency this router cannot operate without.
103
+
104
+ Every field defaults to ``None`` so a test can build a context holding
105
+ only what it exercises. A router that binds an absent dependency used
106
+ to discover it as ``TypeError: 'NoneType' object is not callable``
107
+ inside a request handler, far from the wiring mistake. Binding through
108
+ ``require`` moves that to router construction with the field named.
109
+
110
+ It is also what makes the binding statically honest: the declared type
111
+ is ``Optional[...]``, so a type checker reports every call site as
112
+ ``"None" not callable`` — 105 of them in the workspace router alone.
113
+ ``require`` states the precondition once instead.
114
+ """
115
+ if field not in self.__dataclass_fields__:
116
+ raise AttributeError(f"AppContext has no field {field!r}")
117
+ value = getattr(self, field)
118
+ if value is None:
119
+ raise RuntimeError(
120
+ f"AppContext.{field} is required by this router but was not provided"
121
+ )
122
+ return value
@@ -16,7 +16,7 @@ from typing import Any, Dict, List
16
16
 
17
17
  from latticeai.core.legacy_compatibility import legacy_shim_report
18
18
 
19
- ARCHITECTURE_VERSION_TARGET = "10.2.0"
19
+ ARCHITECTURE_VERSION_TARGET = "10.4.0"
20
20
 
21
21
  PREFERRED_REFACTORING_ORDER = [
22
22
  "agent-runtime",
@@ -143,7 +143,7 @@ class BrainIntelligenceService:
143
143
  stale_cutoff = now - timedelta(days=_STALE_DAYS)
144
144
  dated = [(_parse_ts(n.get("updated_at")), n) for n in nodes]
145
145
  known = [pair for pair in dated if pair[0] is not None]
146
- stale = [n for ts, n in known if ts < stale_cutoff]
146
+ stale = [n for ts, n in known if ts is not None and ts < stale_cutoff]
147
147
  fresh_ratio = 1.0 - (len(stale) / len(known)) if known else 0.0
148
148
  dimensions["freshness"] = {
149
149
  "status": "ok",
@@ -576,7 +576,9 @@ class BrainIntelligenceService:
576
576
  for c in conflicts
577
577
  ):
578
578
  continue
579
- other = next((r for r in memory_rows if r["id"] == other_id), None)
579
+ other: Dict[str, Any] | None = next(
580
+ (r for r in memory_rows if r["id"] == other_id), None
581
+ )
580
582
  conflicts.append({
581
583
  "kind": "memory_pair",
582
584
  "left_id": candidate.id,
@@ -46,7 +46,9 @@ class CloudTurnResult:
46
46
 
47
47
 
48
48
  class CloudLLMAdapter(Protocol):
49
- async def stream(
49
+ # `def`, not `async def`: implementations are async generators, so the
50
+ # call itself returns the iterator rather than a coroutine wrapping it.
51
+ def stream(
50
52
  self,
51
53
  *,
52
54
  system: str,
@@ -107,7 +109,7 @@ class CloudStreamingBridge:
107
109
  answer_text=answer,
108
110
  sent_node_ids=list(minimal.node_ids),
109
111
  provider=getattr(self._adapter, "provider_name", "cloud"),
110
- model=model or getattr(self._adapter, "default_model", ""),
112
+ model=str(model or getattr(self._adapter, "default_model", "")),
111
113
  )
112
114
 
113
115
 
@@ -157,7 +159,7 @@ def plan_kg_expansion(result: CloudTurnResult) -> KGExpansionPlan:
157
159
  )
158
160
 
159
161
  return KGExpansionPlan(
160
- conversation_title=conv_node["title"],
162
+ conversation_title=str(conv_node["title"]),
161
163
  new_nodes=[conv_node],
162
164
  new_edges=edges,
163
165
  provenance={
@@ -185,7 +185,7 @@ class CommandCenterService:
185
185
  "drafts": drafts,
186
186
  }
187
187
  if last_execution is not None:
188
- section["last_execution"] = last_execution
188
+ section["last_execution"] = last_execution # type: ignore[assignment]
189
189
  return section
190
190
 
191
191
  def _review_section(
@@ -150,7 +150,8 @@ class EvidenceActionService:
150
150
  missing.append(node_id)
151
151
  continue
152
152
  # /api/graph/node wraps the record; get_node returns it directly.
153
- record = node.get("node") if isinstance(node.get("node"), Mapping) else node
153
+ wrapped = node.get("node")
154
+ record: Mapping[str, Any] = wrapped if isinstance(wrapped, Mapping) else node
154
155
  title = str(record.get("title") or record.get("id") or node_id)
155
156
  body = str(record.get("summary") or record.get("content") or "").strip()
156
157
  metadata = record.get("metadata")
@@ -202,7 +202,7 @@ async def stream_hybrid_cloud_turn(
202
202
  answer_text=answer,
203
203
  sent_node_ids=list(minimal.node_ids),
204
204
  provider=getattr(chosen_adapter, "provider_name", "cloud"),
205
- model=model or getattr(chosen_adapter, "default_model", ""),
205
+ model=str(model or getattr(chosen_adapter, "default_model", "")),
206
206
  )
207
207
  else:
208
208
  result = await bridge.run_turn(
@@ -57,8 +57,9 @@ class LocalKnowledgeWatcher:
57
57
  self._hooks = hooks
58
58
  self._lock = threading.Lock()
59
59
  self._watched: Dict[str, Dict[str, Any]] = {}
60
- self._observer_cls = None
61
- self._event_handler_base = None
60
+ # watchdog is optional; these stay None when it is not installed.
61
+ self._observer_cls: Optional[Any] = None
62
+ self._event_handler_base: Optional[Any] = None
62
63
  self._import_error = ""
63
64
  try:
64
65
  from watchdog.events import FileSystemEventHandler
@@ -127,12 +128,21 @@ class LocalKnowledgeWatcher:
127
128
 
128
129
  self.stop_source(source_id)
129
130
 
130
- class Handler(_LocalWatchHandler, self._event_handler_base): # type: ignore[misc, valid-type]
131
+ handler_base = self._event_handler_base
132
+ observer_cls = self._observer_cls
133
+ if handler_base is None or observer_cls is None:
134
+ return {
135
+ "watching": False,
136
+ "source_id": source_id,
137
+ "error": self._import_error or "watchdog is not installed",
138
+ }
139
+
140
+ class Handler(_LocalWatchHandler, handler_base): # type: ignore[misc, valid-type]
131
141
  def __init__(handler_self):
132
- self._event_handler_base.__init__(handler_self)
142
+ handler_base.__init__(handler_self) # type: ignore[misc]
133
143
  _LocalWatchHandler.__init__(handler_self, lambda: self._schedule(source_id))
134
144
 
135
- observer = self._observer_cls()
145
+ observer = observer_cls()
136
146
  try:
137
147
  observer.schedule(Handler(), str(root), recursive=True)
138
148
  observer.start()
@@ -161,6 +171,8 @@ class LocalKnowledgeWatcher:
161
171
  timer.cancel()
162
172
  observer = item.get("observer")
163
173
  try:
174
+ if observer is None:
175
+ raise RuntimeError("watch entry has no observer")
164
176
  observer.stop()
165
177
  observer.join(timeout=3)
166
178
  except Exception as exc:
@@ -285,8 +297,10 @@ def create_local_knowledge_router(
285
297
  async def knowledge_graph_local_sources(request: Request):
286
298
  require_user(request)
287
299
  payload = graph().local_sources()
288
- watch_status = watcher.status() if watcher else {"available": False, "active": {}}
289
- active = watch_status.get("active", {})
300
+ watch_status: Dict[str, Any] = (
301
+ watcher.status() if watcher else {"available": False, "active": {}}
302
+ )
303
+ active: Dict[str, Any] = dict(watch_status.get("active") or {})
290
304
  for source in payload.get("sources", []):
291
305
  source["watch_active"] = source.get("id") in active
292
306
  source["watch_status"] = active.get(source.get("id"))
@@ -305,8 +319,10 @@ def create_local_knowledge_router(
305
319
  require_user(request)
306
320
  kg = graph()
307
321
  payload = kg.local_source_health(error_samples=error_samples)
308
- watch_status = watcher.status() if watcher else {"available": False, "active": {}}
309
- active = watch_status.get("active", {})
322
+ watch_status: Dict[str, Any] = (
323
+ watcher.status() if watcher else {"available": False, "active": {}}
324
+ )
325
+ active = dict(watch_status.get("active") or {})
310
326
  for folder in payload.get("folders", []):
311
327
  folder["watch_active"] = folder.get("id") in active
312
328
  payload["watch"] = watch_status