superlocalmemory 3.7.8 → 3.8.1

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 (280) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/ATTRIBUTION.md +1 -3
  3. package/CHANGELOG.md +129 -0
  4. package/README.md +205 -123
  5. package/package.json +12 -3
  6. package/plugin/.claude-plugin/plugin.json +2 -3
  7. package/plugin/CLAUDE.md +8 -8
  8. package/plugin/agents/slm-governance-advisor.md +80 -0
  9. package/plugin/agents/slm-loop-runner.md +71 -0
  10. package/plugin/agents/slm-memory-advisor.md +10 -5
  11. package/plugin/agents/slm-optimize-advisor.md +9 -3
  12. package/plugin/commands/slm-loop.md +31 -0
  13. package/plugin/hooks/hooks.json +79 -0
  14. package/plugin/requirements.txt +1 -1
  15. package/plugin/scripts/slm-launch +46 -7
  16. package/plugin/settings.json +9 -0
  17. package/plugin/skills/slm-cache/SKILL.md +9 -1
  18. package/plugin/skills/slm-compress/SKILL.md +8 -1
  19. package/plugin/skills/slm-governance/SKILL.md +248 -0
  20. package/plugin/skills/slm-graph/SKILL.md +17 -3
  21. package/plugin/skills/slm-loop/SKILL.md +99 -0
  22. package/plugin/skills/slm-mesh/SKILL.md +282 -0
  23. package/plugin/skills/slm-profile/SKILL.md +148 -0
  24. package/plugin/skills/slm-recall/SKILL.md +46 -10
  25. package/plugin/skills/slm-remember/SKILL.md +48 -1
  26. package/plugin/skills/slm-scope/SKILL.md +176 -0
  27. package/plugin/skills/slm-session/SKILL.md +24 -1
  28. package/plugin/skills/slm-status/SKILL.md +18 -1
  29. package/plugin-src/rules/AGENTS.md +57 -18
  30. package/plugin-src/skills/slm-cache/SKILL.md +9 -1
  31. package/plugin-src/skills/slm-compress/SKILL.md +8 -1
  32. package/plugin-src/skills/slm-graph/SKILL.md +17 -3
  33. package/plugin-src/skills/slm-recall/SKILL.md +46 -10
  34. package/plugin-src/skills/slm-remember/SKILL.md +48 -1
  35. package/plugin-src/skills/slm-session/SKILL.md +24 -1
  36. package/plugin-src/skills/slm-status/SKILL.md +18 -1
  37. package/pyproject.toml +2 -1
  38. package/scripts/postinstall/validation.js +2 -0
  39. package/scripts/postinstall-interactive.js +74 -2
  40. package/src/superlocalmemory/__init__.py +1 -1
  41. package/src/superlocalmemory/access/__init__.py +3 -0
  42. package/src/superlocalmemory/access/rbac.py +477 -0
  43. package/src/superlocalmemory/cli/commands.py +228 -17
  44. package/src/superlocalmemory/cli/compress_cmd.py +17 -7
  45. package/src/superlocalmemory/cli/daemon.py +7 -0
  46. package/src/superlocalmemory/cli/loop_cmd.py +187 -0
  47. package/src/superlocalmemory/cli/main.py +49 -8
  48. package/src/superlocalmemory/cli/mesh_cmd.py +38 -0
  49. package/src/superlocalmemory/cli/optimize_cmd.py +3 -0
  50. package/src/superlocalmemory/cli/pending_store.py +49 -13
  51. package/src/superlocalmemory/cli/proxy_cmd.py +4 -0
  52. package/src/superlocalmemory/cli/scale_engine_cmd.py +6 -0
  53. package/src/superlocalmemory/cli/setup_wizard.py +22 -13
  54. package/src/superlocalmemory/cli/version_banner.py +17 -3
  55. package/src/superlocalmemory/compliance/audit.py +6 -0
  56. package/src/superlocalmemory/compliance/gdpr.py +128 -138
  57. package/src/superlocalmemory/compliance/retention.py +176 -45
  58. package/src/superlocalmemory/core/backend_orchestrator.py +23 -59
  59. package/src/superlocalmemory/core/community_summary.py +267 -0
  60. package/src/superlocalmemory/core/config.py +216 -3
  61. package/src/superlocalmemory/core/consolidation_engine.py +95 -22
  62. package/src/superlocalmemory/core/context_cache.py +61 -18
  63. package/src/superlocalmemory/core/embedding_worker.py +21 -7
  64. package/src/superlocalmemory/core/embeddings.py +131 -46
  65. package/src/superlocalmemory/core/engine.py +41 -22
  66. package/src/superlocalmemory/core/engine_ingestion.py +359 -43
  67. package/src/superlocalmemory/core/engine_wiring.py +13 -0
  68. package/src/superlocalmemory/core/entity_community.py +178 -0
  69. package/src/superlocalmemory/core/graph_analyzer.py +39 -2
  70. package/src/superlocalmemory/core/graph_pruner.py +13 -8
  71. package/src/superlocalmemory/core/ingestion_command.py +134 -25
  72. package/src/superlocalmemory/core/injection.py +12 -7
  73. package/src/superlocalmemory/core/key_expander.py +138 -0
  74. package/src/superlocalmemory/core/maintenance.py +23 -0
  75. package/src/superlocalmemory/core/maintenance_scheduler.py +17 -7
  76. package/src/superlocalmemory/core/modes.py +1 -1
  77. package/src/superlocalmemory/core/mutations.py +2 -2
  78. package/src/superlocalmemory/core/pii.py +105 -0
  79. package/src/superlocalmemory/core/progressive_abstraction.py +208 -0
  80. package/src/superlocalmemory/core/recall_pipeline.py +7 -3
  81. package/src/superlocalmemory/core/recall_worker.py +20 -6
  82. package/src/superlocalmemory/core/scale_engine.py +60 -1
  83. package/src/superlocalmemory/core/security_primitives.py +40 -2
  84. package/src/superlocalmemory/core/store_pipeline.py +186 -29
  85. package/src/superlocalmemory/core/worker_pool.py +21 -6
  86. package/src/superlocalmemory/encoding/entity_reflexion.py +200 -0
  87. package/src/superlocalmemory/encoding/entity_resolver.py +34 -24
  88. package/src/superlocalmemory/encoding/fact_extractor.py +26 -1
  89. package/src/superlocalmemory/encoding/temporal_validator.py +64 -1
  90. package/src/superlocalmemory/evolution/evolution_store.py +122 -45
  91. package/src/superlocalmemory/evolution/llm_dispatch.py +12 -1
  92. package/src/superlocalmemory/evolution/model_selection.py +160 -0
  93. package/src/superlocalmemory/evolution/mutation_generator.py +16 -0
  94. package/src/superlocalmemory/evolution/skill_evolver.py +127 -42
  95. package/src/superlocalmemory/evolution/triggers.py +22 -13
  96. package/src/superlocalmemory/graph/cozo_backend.py +43 -20
  97. package/src/superlocalmemory/hooks/adapter_base.py +5 -1
  98. package/src/superlocalmemory/hooks/auto_recall.py +13 -1
  99. package/src/superlocalmemory/hooks/claude_code_hooks.py +11 -0
  100. package/src/superlocalmemory/hooks/codex_assets.py +64 -5
  101. package/src/superlocalmemory/hooks/hook_daemon.py +20 -3
  102. package/src/superlocalmemory/hooks/hook_handlers.py +6 -1
  103. package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
  104. package/src/superlocalmemory/hooks/portable_kit.py +148 -3
  105. package/src/superlocalmemory/infra/backup.py +12 -1
  106. package/src/superlocalmemory/infra/daemon_identity.py +40 -4
  107. package/src/superlocalmemory/infra/data_root.py +43 -4
  108. package/src/superlocalmemory/infra/event_bus.py +107 -24
  109. package/src/superlocalmemory/infra/rate_limiter.py +93 -0
  110. package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
  111. package/src/superlocalmemory/ingestion/credentials.py +1 -1
  112. package/src/superlocalmemory/learning/cross_project.py +28 -19
  113. package/src/superlocalmemory/learning/model_rollback.py +3 -0
  114. package/src/superlocalmemory/learning/ranker_retrain_online.py +2 -0
  115. package/src/superlocalmemory/learning/reward.py +50 -0
  116. package/src/superlocalmemory/learning/reward_proxy.py +42 -9
  117. package/src/superlocalmemory/learning/source_quality.py +523 -1
  118. package/src/superlocalmemory/loops/__init__.py +56 -0
  119. package/src/superlocalmemory/loops/budget.py +58 -0
  120. package/src/superlocalmemory/loops/engine.py +164 -0
  121. package/src/superlocalmemory/loops/ledger.py +263 -0
  122. package/src/superlocalmemory/loops/models.py +152 -0
  123. package/src/superlocalmemory/loops/rules.py +52 -0
  124. package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
  125. package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
  126. package/src/superlocalmemory/mcp/profiles.py +103 -0
  127. package/src/superlocalmemory/mcp/server.py +32 -79
  128. package/src/superlocalmemory/mcp/tools_active.py +4 -7
  129. package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
  130. package/src/superlocalmemory/mcp/tools_core.py +12 -4
  131. package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
  132. package/src/superlocalmemory/mcp/tools_learning.py +2 -2
  133. package/src/superlocalmemory/mcp/tools_loops.py +300 -0
  134. package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
  135. package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
  136. package/src/superlocalmemory/mesh/broker.py +237 -129
  137. package/src/superlocalmemory/mesh/remote_sync.py +50 -8
  138. package/src/superlocalmemory/optimize/NOTICE +1 -6
  139. package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
  140. package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
  141. package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
  142. package/src/superlocalmemory/optimize/compress/align.py +32 -26
  143. package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
  144. package/src/superlocalmemory/optimize/compress/router.py +105 -22
  145. package/src/superlocalmemory/optimize/config/defaults.py +1 -1
  146. package/src/superlocalmemory/optimize/config/schema.py +87 -4
  147. package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
  148. package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
  149. package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
  150. package/src/superlocalmemory/optimize/storage/db.py +38 -9
  151. package/src/superlocalmemory/optimize/storage/schema.py +10 -0
  152. package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
  153. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  154. package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
  155. package/src/superlocalmemory/retrieval/engine.py +221 -47
  156. package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
  157. package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
  158. package/src/superlocalmemory/retrieval/reranker.py +3 -4
  159. package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
  160. package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
  161. package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
  162. package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
  163. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
  164. package/src/superlocalmemory/retrieval/time_window.py +181 -0
  165. package/src/superlocalmemory/server/api.py +4 -4
  166. package/src/superlocalmemory/server/config_file.py +90 -0
  167. package/src/superlocalmemory/server/origin.py +50 -0
  168. package/src/superlocalmemory/server/profile_runtime.py +125 -8
  169. package/src/superlocalmemory/server/rbac_enforce.py +142 -0
  170. package/src/superlocalmemory/server/recall_health.py +24 -3
  171. package/src/superlocalmemory/server/recall_serializer.py +19 -1
  172. package/src/superlocalmemory/server/routes/abstraction.py +115 -0
  173. package/src/superlocalmemory/server/routes/agents.py +128 -38
  174. package/src/superlocalmemory/server/routes/backup.py +317 -70
  175. package/src/superlocalmemory/server/routes/behavioral.py +349 -71
  176. package/src/superlocalmemory/server/routes/brain.py +69 -12
  177. package/src/superlocalmemory/server/routes/chat.py +10 -5
  178. package/src/superlocalmemory/server/routes/compliance.py +171 -21
  179. package/src/superlocalmemory/server/routes/config_api.py +438 -0
  180. package/src/superlocalmemory/server/routes/data_io.py +30 -8
  181. package/src/superlocalmemory/server/routes/entity.py +108 -26
  182. package/src/superlocalmemory/server/routes/events.py +24 -8
  183. package/src/superlocalmemory/server/routes/evolution.py +189 -68
  184. package/src/superlocalmemory/server/routes/helpers.py +16 -1
  185. package/src/superlocalmemory/server/routes/ingest.py +7 -4
  186. package/src/superlocalmemory/server/routes/insights.py +3 -3
  187. package/src/superlocalmemory/server/routes/learning.py +289 -118
  188. package/src/superlocalmemory/server/routes/learning_telemetry.py +153 -0
  189. package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
  190. package/src/superlocalmemory/server/routes/memories.py +182 -57
  191. package/src/superlocalmemory/server/routes/mesh.py +200 -31
  192. package/src/superlocalmemory/server/routes/optimize.py +33 -1
  193. package/src/superlocalmemory/server/routes/prewarm.py +2 -0
  194. package/src/superlocalmemory/server/routes/profiles.py +63 -17
  195. package/src/superlocalmemory/server/routes/ratelimit.py +132 -0
  196. package/src/superlocalmemory/server/routes/rbac.py +367 -0
  197. package/src/superlocalmemory/server/routes/stats.py +103 -158
  198. package/src/superlocalmemory/server/routes/tiers.py +11 -9
  199. package/src/superlocalmemory/server/routes/token.py +3 -13
  200. package/src/superlocalmemory/server/routes/v3_api.py +247 -89
  201. package/src/superlocalmemory/server/routes/ws.py +5 -2
  202. package/src/superlocalmemory/server/security_middleware.py +12 -5
  203. package/src/superlocalmemory/server/ui.py +20 -5
  204. package/src/superlocalmemory/server/unified_daemon.py +827 -72
  205. package/src/superlocalmemory/server/write_identity.py +38 -8
  206. package/src/superlocalmemory/storage/database.py +265 -53
  207. package/src/superlocalmemory/storage/migration_runner.py +132 -1
  208. package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +5 -0
  209. package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +108 -0
  210. package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +86 -0
  211. package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +194 -0
  212. package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +87 -0
  213. package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +90 -0
  214. package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +136 -0
  215. package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +163 -0
  216. package/src/superlocalmemory/storage/migrations/M028_fact_entity_associations.py +270 -0
  217. package/src/superlocalmemory/storage/migrations/M029_behavioral_history_indexes.py +137 -0
  218. package/src/superlocalmemory/storage/migrations/M030_entity_explorer_indexes.py +93 -0
  219. package/src/superlocalmemory/storage/migrations/__init__.py +4 -0
  220. package/src/superlocalmemory/storage/models.py +4 -0
  221. package/src/superlocalmemory/storage/schema.py +136 -1
  222. package/src/superlocalmemory/storage/schema_v32.py +2 -0
  223. package/src/superlocalmemory/storage/schema_v343.py +24 -12
  224. package/src/superlocalmemory/storage/schema_v347.py +4 -0
  225. package/src/superlocalmemory/trust/gate.py +49 -8
  226. package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
  227. package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
  228. package/src/superlocalmemory/ui/css/design-system.css +621 -0
  229. package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
  230. package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
  231. package/src/superlocalmemory/ui/favicon.svg +35 -4
  232. package/src/superlocalmemory/ui/index.html +303 -173
  233. package/src/superlocalmemory/ui/js/brain.js +5 -20
  234. package/src/superlocalmemory/ui/js/core.js +100 -41
  235. package/src/superlocalmemory/ui/js/dashboard.js +403 -65
  236. package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
  237. package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
  238. package/src/superlocalmemory/ui/js/math-health.js +1 -1
  239. package/src/superlocalmemory/ui/js/memories.js +15 -4
  240. package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
  241. package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
  242. package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
  243. package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
  244. package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
  245. package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
  246. package/src/superlocalmemory/ui/js/od-agents.js +544 -0
  247. package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
  248. package/src/superlocalmemory/ui/js/od-backup.js +871 -0
  249. package/src/superlocalmemory/ui/js/od-brain.js +816 -0
  250. package/src/superlocalmemory/ui/js/od-entities.js +579 -0
  251. package/src/superlocalmemory/ui/js/od-graph.js +600 -0
  252. package/src/superlocalmemory/ui/js/od-health.js +539 -0
  253. package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
  254. package/src/superlocalmemory/ui/js/od-memories.js +929 -0
  255. package/src/superlocalmemory/ui/js/od-mesh.js +553 -0
  256. package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
  257. package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
  258. package/src/superlocalmemory/ui/js/od-settings.js +1107 -0
  259. package/src/superlocalmemory/ui/js/od-shell.js +809 -0
  260. package/src/superlocalmemory/ui/js/od-skills.js +600 -0
  261. package/src/superlocalmemory/ui/js/od-team.js +258 -0
  262. package/src/superlocalmemory/ui/js/profiles.js +159 -46
  263. package/src/superlocalmemory/ui/js/settings.js +17 -3
  264. package/src/superlocalmemory/ui/js/timeline.js +34 -5
  265. package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
  266. package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
  267. package/plugin-src/.mcp.json +0 -12
  268. package/plugin-src/agents/slm-memory-advisor.md +0 -44
  269. package/plugin-src/agents/slm-optimize-advisor.md +0 -38
  270. package/plugin-src/hooks/.gitkeep +0 -0
  271. package/plugin-src/hooks/hooks.json +0 -23
  272. package/plugin-src/manifest.json +0 -25
  273. package/plugin-src/requirements.txt +0 -1
  274. package/plugin-src/rules/CLAUDE.md.fragment +0 -44
  275. package/plugin-src/scripts/ensure-venv.bat +0 -122
  276. package/plugin-src/scripts/ensure-venv.sh +0 -105
  277. package/plugin-src/scripts/slm-launch +0 -23
  278. package/plugin-src/scripts/slm-launch.bat +0 -23
  279. package/plugin-src/settings.json +0 -16
  280. package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
@@ -38,7 +38,11 @@ from typing import Protocol, runtime_checkable
38
38
  # ---------------------------------------------------------------------------
39
39
 
40
40
  HARD_BYTES_CAP = 4096
41
- COPILOT_SOFT_BYTES = 2048
41
+ # Soft budget for the managed instruction block. Raised 2048 -> 2560 -> 2816 as
42
+ # the block grew to carry the memory, token-optimization, and (compact)
43
+ # bounded-loop protocols; the 4 KB hard cap still bounds total size (recall
44
+ # content is truncated to stay under it).
45
+ COPILOT_SOFT_BYTES = 2816
42
46
  TRUNCATION_MARKER = b"\n<!-- truncated -->"
43
47
 
44
48
 
@@ -10,6 +10,7 @@ import logging
10
10
  from typing import Any, Callable
11
11
 
12
12
  from superlocalmemory.core.injection import InjectableMemory, render_context
13
+ from superlocalmemory.retrieval.temporal_frame import relative_age, temporal_frame
13
14
 
14
15
  logger = logging.getLogger(__name__)
15
16
 
@@ -85,7 +86,13 @@ class AutoRecall:
85
86
  )
86
87
  for r in relevant[:self._max_memories]
87
88
  ]
88
- return render_context(memories, mode="B", cfg=None, wrap=True)
89
+ ctx = render_context(memories, mode="B", cfg=None, wrap=True)
90
+ # T-inject: anchor the injected memories to "now" so a time-blind
91
+ # model can weigh recency. Prepend a one-line temporal frame.
92
+ frame = temporal_frame(
93
+ [getattr(r.fact, "created_at", "") for r in relevant[:self._max_memories]]
94
+ )
95
+ return f"{frame}\n\n{ctx}" if ctx else ctx
89
96
  except Exception as exc:
90
97
  logger.warning("Auto-recall failed: %s", exc)
91
98
  return ""
@@ -104,12 +111,17 @@ class AutoRecall:
104
111
  response = self._recall(query, self._max_memories)
105
112
  if response is None:
106
113
  return []
114
+ from datetime import datetime as _dt, timezone as _tz
115
+ _now = _dt.now(_tz.utc)
107
116
  results = []
108
117
  for r in response.results:
109
118
  if r.score >= self._threshold:
119
+ _created = getattr(r.fact, "created_at", "") or ""
110
120
  results.append({
111
121
  "fact_id": r.fact.fact_id,
112
122
  "content": r.fact.content[:300],
123
+ "created_at": _created,
124
+ "age_label": relative_age(_created, _now),
113
125
  "score": round(r.score, 3),
114
126
  "relevance_score": round(
115
127
  getattr(r, "relevance_score", r.score) or 0.0, 3
@@ -261,6 +261,17 @@ def _hook_definitions(include_gate: bool = False) -> dict[str, list]:
261
261
  "command": _wrap_python_cmd("stop_outcome"),
262
262
  "timeout": 10000,
263
263
  },
264
+ # Commit temporal summaries so session decisions survive beyond
265
+ # the git-state snapshot written by `slm hook stop`.
266
+ {
267
+ "type": "command",
268
+ "command": (
269
+ 'cmd /c "slm session close 2>NUL || exit /b 0"'
270
+ if sys.platform == "win32"
271
+ else "slm session close 2>/dev/null || true"
272
+ ),
273
+ "timeout": 15000,
274
+ },
264
275
  ]
265
276
  }
266
277
  ],
@@ -7,10 +7,16 @@ import sysconfig
7
7
  from pathlib import Path
8
8
 
9
9
  SKILLS = ("slm-cache", "slm-compress", "slm-graph", "slm-recall", "slm-remember", "slm-session", "slm-status")
10
- AGENTS = {
11
- "slm-memory-advisor.toml": 'name = "slm-memory-advisor"\ndescription = "Use SuperLocalMemory safely: initialize once, recall before remember, and store only durable atomic facts."\ninstructions = "Use SLM for memory discipline only. Check results before claiming success; preserve private scope unless the user explicitly asks to share."\n',
12
- "slm-optimize-advisor.toml": 'name = "slm-optimize-advisor"\ndescription = "Analyze SuperLocalMemory retrieval, ingestion, cache, compression, and optimization evidence."\ninstructions = "Inspect real SLM evidence before advising. Separate observed performance from targets and recommend measurable experiments."\n',
13
- }
10
+
11
+ # Codex subagent files written to ~/.codex/agents (content built by _agent_files()).
12
+ AGENTS = ("slm-memory-advisor.toml", "slm-optimize-advisor.toml")
13
+
14
+ _MEMORY_ADVISOR_TOML = (
15
+ 'name = "slm-memory-advisor"\n'
16
+ 'description = "Use SuperLocalMemory safely: initialize once, recall before remember, and store only durable atomic facts."\n'
17
+ 'instructions = "Use SLM for memory discipline only. Check results before claiming success; preserve private scope unless the user explicitly asks to share."\n'
18
+ )
19
+
14
20
 
15
21
  def _source_root() -> Path:
16
22
  development = Path(__file__).resolve().parents[3] / "plugin-src" / "skills"
@@ -21,6 +27,57 @@ def _source_root() -> Path:
21
27
  return installed
22
28
  raise FileNotFoundError("Bundled Codex skills were not found in this installation")
23
29
 
30
+
31
+ def _agents_source_root() -> Path | None:
32
+ development = Path(__file__).resolve().parents[3] / "plugin-src" / "agents"
33
+ if development.exists():
34
+ return development
35
+ installed = Path(sysconfig.get_path("data")) / "share" / "superlocalmemory" / "codex" / "agents"
36
+ return installed if installed.exists() else None
37
+
38
+
39
+ def _optimize_advisor_toml() -> str:
40
+ """Build the optimize-advisor TOML from the canonical advisor doc so Codex
41
+ ships the FULL decision rules (the 8-rule tree), not a one-line stub. Falls
42
+ back to a short instruction only if the source doc is unavailable.
43
+ """
44
+ description = (
45
+ "Apply SuperLocalMemory's no-proxy context-optimization rules — reversible "
46
+ "compression of large tool output and KV-caching of repeated reads/searches."
47
+ )
48
+ body = ""
49
+ root = _agents_source_root()
50
+ if root is not None:
51
+ src = root / "slm-optimize-advisor.md"
52
+ if src.exists():
53
+ text = src.read_text(encoding="utf-8")
54
+ if text.startswith("---"): # strip YAML frontmatter, keep the guidance body
55
+ end = text.find("\n---", 3)
56
+ if end != -1:
57
+ text = text[end + 4:]
58
+ body = text.strip()
59
+ if not body:
60
+ body = (
61
+ "Reduce context-window pressure with the Surface-B tools (reversible CCR "
62
+ "compression + a per-agent KV cache); fail-open — never block the task."
63
+ )
64
+ # TOML literal multi-line string ('''...'''): no escape processing, and the
65
+ # advisor body contains no ''' sequence.
66
+ return (
67
+ 'name = "slm-optimize-advisor"\n'
68
+ f'description = "{description}"\n'
69
+ f"instructions = '''\n{body}\n'''\n"
70
+ )
71
+
72
+
73
+ def _agent_files() -> dict:
74
+ """Return {filename: TOML content} for the Codex subagents."""
75
+ return {
76
+ "slm-memory-advisor.toml": _MEMORY_ADVISOR_TOML,
77
+ "slm-optimize-advisor.toml": _optimize_advisor_toml(),
78
+ }
79
+
80
+
24
81
  def install_assets(*, home: Path | None = None, dry_run: bool = False) -> dict:
25
82
  """Copy only named SLM assets; never rewrite user-owned assets."""
26
83
  home = home or Path.home()
@@ -37,10 +94,11 @@ def install_assets(*, home: Path | None = None, dry_run: bool = False) -> dict:
37
94
  target = skills_root / skill
38
95
  target.mkdir(parents=True, exist_ok=True)
39
96
  shutil.copy2(source / skill / "SKILL.md", target / "SKILL.md")
40
- for filename, content in AGENTS.items():
97
+ for filename, content in _agent_files().items():
41
98
  (agents_root / filename).write_text(content, encoding="utf-8")
42
99
  return {"success": True, "skills": list(SKILLS), "agents": list(AGENTS), "dry_run": False}
43
100
 
101
+
44
102
  def remove_assets(*, home: Path | None = None, dry_run: bool = False) -> dict:
45
103
  """Remove only the known SLM directories and files."""
46
104
  home = home or Path.home()
@@ -52,6 +110,7 @@ def remove_assets(*, home: Path | None = None, dry_run: bool = False) -> dict:
52
110
  shutil.rmtree(target) if target.is_dir() else target.unlink()
53
111
  return {"success": True, "removed": [str(x) for x in existing], "dry_run": dry_run}
54
112
 
113
+
55
114
  def status_assets(*, home: Path | None = None) -> dict:
56
115
  home = home or Path.home()
57
116
  skills = [x for x in SKILLS if (home / ".agents" / "skills" / x / "SKILL.md").exists()]
@@ -25,6 +25,7 @@ import json
25
25
  import logging
26
26
  import os
27
27
  import socket
28
+ import sys
28
29
  import threading
29
30
  import time
30
31
  from pathlib import Path
@@ -35,6 +36,12 @@ logger = logging.getLogger(__name__)
35
36
 
36
37
  _DEFAULT_SOCK_NAME = "hook_daemon.sock"
37
38
 
39
+ # AF_UNIX is absent on Windows builds < 10.0.17063 and on Python < 3.9. When it
40
+ # is unavailable the hook daemon does not start and callers fall back to the
41
+ # subprocess recall path. Detect it explicitly (once, with a log) instead of
42
+ # relying on an AttributeError being swallowed by a broad except.
43
+ _AF_UNIX = getattr(socket, "AF_UNIX", None)
44
+
38
45
 
39
46
  def _default_sock_path() -> Path:
40
47
  return state_path(_DEFAULT_SOCK_NAME)
@@ -77,7 +84,13 @@ class HookDaemon:
77
84
  from superlocalmemory.core.recall_queue import RecallQueue
78
85
  self._queue = RecallQueue(self._queue_db_path)
79
86
 
80
- self._server_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
87
+ if _AF_UNIX is None:
88
+ logger.info(
89
+ "HookDaemon: AF_UNIX unavailable on %s; hook recall uses the "
90
+ "subprocess fallback", sys.platform,
91
+ )
92
+ raise RuntimeError("AF_UNIX unavailable on this platform")
93
+ self._server_sock = socket.socket(_AF_UNIX, socket.SOCK_STREAM)
81
94
  self._server_sock.bind(str(self._sock_path))
82
95
  self._server_sock.listen(8)
83
96
  self._server_sock.settimeout(1.0)
@@ -226,9 +239,11 @@ def try_socket_recall(
226
239
  path = sock_path or _default_sock_path()
227
240
  if not path.exists():
228
241
  return None
242
+ if _AF_UNIX is None:
243
+ return None
229
244
 
230
245
  try:
231
- client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
246
+ client = socket.socket(_AF_UNIX, socket.SOCK_STREAM)
232
247
  client.settimeout(timeout)
233
248
  client.connect(str(path))
234
249
 
@@ -259,10 +274,12 @@ def ensure_hook_daemon(
259
274
  ) -> HookDaemon | None:
260
275
  """Start hook daemon if not already running. Returns daemon or None."""
261
276
  path = sock_path or _default_sock_path()
277
+ if _AF_UNIX is None:
278
+ return None
262
279
 
263
280
  if path.exists():
264
281
  try:
265
- test = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
282
+ test = socket.socket(_AF_UNIX, socket.SOCK_STREAM)
266
283
  test.settimeout(1.0)
267
284
  test.connect(str(path))
268
285
  test.close()
@@ -26,6 +26,8 @@ import urllib.error
26
26
  import urllib.request
27
27
  from pathlib import Path
28
28
 
29
+ from superlocalmemory import __version__
30
+
29
31
  # ---------------------------------------------------------------------------
30
32
  # Cross-platform temp paths
31
33
  # ---------------------------------------------------------------------------
@@ -221,7 +223,10 @@ def _codex_mcp_session_init(project_dir: str, payload: dict) -> dict:
221
223
  proc.stdin.write(json.dumps({
222
224
  "jsonrpc": "2.0", "id": 1, "method": "initialize",
223
225
  "params": {"protocolVersion": "2024-11-05", "capabilities": {},
224
- "clientInfo": {"name": "superlocalmemory-codex-hook", "version": "3.7"}},
226
+ "clientInfo": {
227
+ "name": "superlocalmemory-codex-hook",
228
+ "version": __version__,
229
+ }},
225
230
  }) + "\n")
226
231
  proc.stdin.flush()
227
232
  proc.stdout.readline()
@@ -93,6 +93,58 @@ def memory_protocol_markdown() -> str:
93
93
  "learned>\", \"tags\": \"<comma-separated kebab-case keywords>\"}`.\n"
94
94
  "- A \"substantial task\" is anything you would write a commit "
95
95
  "message or handoff note about — not every tool call.\n"
96
+ "\n"
97
+ + optimize_protocol_markdown()
98
+ + "\n"
99
+ + loop_protocol_markdown()
100
+ )
101
+
102
+
103
+ def optimize_protocol_markdown() -> str:
104
+ """Return the agent-facing token-optimization protocol block (cache/compress).
105
+
106
+ Proxy-free: these MCP tools let the agent losslessly shrink large tool output
107
+ and reuse repeated reads through the same surface, so an agent on any
108
+ front-end (not just Claude Code) can save tokens without a proxy. Fail-open —
109
+ a non-ok result means "use the original and continue".
110
+ """
111
+ return (
112
+ "## Runtime token-optimization protocol (fail-open)\n"
113
+ "SLM can losslessly compress large tool output and cache repeated reads "
114
+ "through the same MCP surface — no proxy required. These calls only save "
115
+ "tokens; if one returns `ok: false`, use the original and continue.\n\n"
116
+ "- **Large tool output (>2000 chars)** → `hub__call_tool` with "
117
+ "`tool=\"slm__slm_compress\"` and `arguments={\"content\": \"<text>\", "
118
+ "\"mode\": \"auto\", \"reversible\": true}`; keep the returned `ccr_id` "
119
+ "and call `tool=\"slm__slm_retrieve\"` if you later need the full "
120
+ "original.\n"
121
+ "- **Repeated reads/searches** → `hub__call_tool` with "
122
+ "`tool=\"slm__slm_cache_get\"` and `arguments={\"key\": \"file:<path>\"}` "
123
+ "first; on a miss, store the result with `tool=\"slm__slm_cache_set\"` "
124
+ "(ttl ~1800).\n"
125
+ "- **Never compress or cache**: code you will edit, JSON you will parse, "
126
+ "secrets, ccr_ids, or anything under ~500 chars.\n"
127
+ )
128
+
129
+
130
+ def loop_protocol_markdown() -> str:
131
+ """Return the agent-facing bounded-loop protocol block.
132
+
133
+ Bounded loops make an agent stop when an INDEPENDENT gate passes — not when
134
+ the agent claims it is done. Persisting each lap to SLM memory makes a run
135
+ auditable and resumable. This block is appended to the shared memory
136
+ protocol so any connected front-end (Claude Code, Codex, Antigravity,
137
+ Cursor, and other IDEs) learns the feature exists and how to reach it.
138
+ """
139
+ return (
140
+ "## Runtime bounded-loop protocol\n"
141
+ "For a task with a checkable gate (tests, schema, lint, reconciliation), "
142
+ "run a *bounded loop*: iterate until an INDEPENDENT gate passes — never "
143
+ "on the agent's own claim, which is advisory only. Try `slm loop demo`; "
144
+ "inspect with `slm loop history` / `slm loop show <run_id>` (each lap "
145
+ "persists as SLM memory, tag `loop:<name>`). Statuses: DONE / HALT / "
146
+ "PAUSE / KILLED / ERROR — report exactly, never as success unless DONE. "
147
+ "Full guide: the slm-loop skill.\n"
96
148
  )
97
149
 
98
150
 
@@ -101,4 +153,6 @@ __all__ = (
101
153
  "SLM_MARKER_END",
102
154
  "strip_slm_block",
103
155
  "memory_protocol_markdown",
156
+ "optimize_protocol_markdown",
157
+ "loop_protocol_markdown",
104
158
  )
@@ -139,7 +139,7 @@ IDE_MATRIX: dict[str, IDEDescriptor] = {
139
139
  mcp_path_project=".mcp.json",
140
140
  server_key="mcpServers",
141
141
  fmt="json",
142
- agents_md_path=None,
142
+ agents_md_path=".junie/AGENTS.md", # Junie guidelines file (GA) — carries memory + optimize protocol
143
143
  server_block={"command": "slm", "args": ["mcp"], "type": "stdio"},
144
144
  caveats="path per product [CN-ONLINE]",
145
145
  ),
@@ -240,11 +240,12 @@ def connect_ide(
240
240
  here: bool = False,
241
241
  profile: str | None = None,
242
242
  agents_md_source: Callable[[], str] | None = None,
243
+ dry_run: bool = False,
243
244
  ) -> dict[str, Any]:
244
245
  """Wire SLM into the target IDE config via merge-not-clobber.
245
246
 
246
247
  Returns a result dict:
247
- {ide, mcp_config: wrote|merged|unchanged|skipped|error,
248
+ {ide, mcp_config: wrote|merged|unchanged|would_write|skipped|error,
248
249
  mcp_path, agents_md: wrote|skipped(...)|unchanged|error,
249
250
  servers_preserved: int, error: str|None}
250
251
  """
@@ -265,6 +266,13 @@ def connect_ide(
265
266
  )
266
267
  return result
267
268
 
269
+ if ide_id == "vscode-copilot" and not here:
270
+ result["error"] = (
271
+ "VS Code / Copilot integration is project-scoped. "
272
+ "Run `slm connect vscode-copilot --here` from the project root."
273
+ )
274
+ return result
275
+
268
276
  # Step 1a — claude-code short-circuit (AC6)
269
277
  if desc.fmt == "":
270
278
  print(CLAUDE_CODE_PLUGIN_POINTER)
@@ -325,7 +333,28 @@ def connect_ide(
325
333
  result["servers_preserved"] = max(0, pre_count - (0 if pre_slm is None else 1))
326
334
  result["mcp_config"] = mcp_status
327
335
 
328
- # Step 6 — atomic write
336
+ # Step 6 — atomic write. A dry run deliberately exercises the merge and
337
+ # packaged-asset lookup path without touching a user-owned IDE config.
338
+ if dry_run:
339
+ result["mcp_config"] = "would_write"
340
+ if desc.agents_md_path is None:
341
+ result["agents_md"] = "skipped(unsupported)"
342
+ elif agents_md_source is None:
343
+ result["agents_md"] = "skipped(no-source)"
344
+ else:
345
+ try:
346
+ source_content = agents_md_source()
347
+ except Exception as exc:
348
+ result["agents_md"] = "error(source-unavailable)"
349
+ result["error"] = f"AGENTS.md asset lookup failed: {exc}"
350
+ return result
351
+ if not isinstance(source_content, str) or not source_content.strip():
352
+ result["agents_md"] = "error(empty-source)"
353
+ result["error"] = "AGENTS.md asset is empty"
354
+ return result
355
+ result["agents_md"] = "would_write"
356
+ return result
357
+
329
358
  try:
330
359
  _atomic_write(config_path, data, desc.fmt)
331
360
  except Exception as exc:
@@ -347,6 +376,54 @@ def connect_ide(
347
376
  return result
348
377
 
349
378
 
379
+ def connect_many(
380
+ ide_ids: list[str],
381
+ *,
382
+ home: Path | None = None,
383
+ project: Path | None = None,
384
+ here: bool = False,
385
+ profile: str | None = None,
386
+ agents_md_source: Callable[[], str] | None = None,
387
+ ) -> list[dict[str, Any]]:
388
+ """Wire SLM into multiple IDE configs via non-destructive merge.
389
+
390
+ Iterates over ``ide_ids`` and calls :func:`connect_ide` for each entry.
391
+ Each IDE is processed independently — a failure on one IDE does NOT abort
392
+ the remaining targets.
393
+
394
+ The underlying :func:`connect_ide` is MERGE-NOT-CLOBBER:
395
+ - Only the ``superlocalmemory`` server key is touched.
396
+ - All other MCP servers + top-level keys are preserved byte-for-byte.
397
+ - Writes are atomic (.tmp + os.replace).
398
+
399
+ Args:
400
+ ide_ids: IDE ids to wire (from :data:`IDE_MATRIX`). Pass an empty
401
+ list to no-op. Unknown ids produce error entries in the output.
402
+ home: Override ``$HOME`` (test hook).
403
+ project: Project root for ``here=True`` installs.
404
+ here: When True, write to project-relative path instead of global.
405
+ profile: Inject ``SLM_MCP_PROFILE`` env-var into every server block.
406
+ agents_md_source: Callable returning AGENTS.md content to append.
407
+
408
+ Returns:
409
+ List of per-IDE result dicts, one per input id. Each dict has the
410
+ same shape as :func:`connect_ide`'s return value::
411
+
412
+ {ide, mcp_config, mcp_path, agents_md, servers_preserved, error}
413
+ """
414
+ return [
415
+ connect_ide(
416
+ ide_id,
417
+ home=home,
418
+ project=project,
419
+ here=here,
420
+ profile=profile,
421
+ agents_md_source=agents_md_source,
422
+ )
423
+ for ide_id in ide_ids
424
+ ]
425
+
426
+
350
427
  # ---------------------------------------------------------------------------
351
428
  # Internal helpers
352
429
  # ---------------------------------------------------------------------------
@@ -480,6 +557,9 @@ def _handle_agents_md(
480
557
  except Exception as exc:
481
558
  logger.warning("agents_md_source() failed: %s — skipping AGENTS.md write", exc)
482
559
  return "skipped(source-error)"
560
+ if not isinstance(source_content, str) or not source_content.strip():
561
+ logger.warning("agents_md_source() returned no content — skipping AGENTS.md write")
562
+ return "skipped(no-source-content)"
483
563
 
484
564
  # Read existing content
485
565
  existing = ""
@@ -504,3 +584,68 @@ def _handle_agents_md(
504
584
  _tmp.write_text(existing + section, encoding="utf-8")
505
585
  os.replace(_tmp, agents_path)
506
586
  return "wrote"
587
+
588
+
589
+ # ---------------------------------------------------------------------------
590
+ # __main__ — `python -m superlocalmemory.hooks.portable_kit [ids...]`
591
+ #
592
+ # Used by the npm installer to execute non-destructive IDE connects when
593
+ # Python is available at postinstall time. If Python / the package is not
594
+ # importable the installer records a pending_ide_connections.json instead.
595
+ # ---------------------------------------------------------------------------
596
+
597
+ if __name__ == "__main__": # pragma: no cover
598
+ import argparse as _argparse
599
+
600
+ _parser = _argparse.ArgumentParser(
601
+ description="SLM IDE connector — non-destructive merge-not-clobber."
602
+ )
603
+ _parser.add_argument(
604
+ "ide_ids",
605
+ nargs="*",
606
+ metavar="IDE",
607
+ help=(
608
+ "IDE ids to connect. Pass 'all' to connect every supported IDE "
609
+ "(excluding claude-code which uses the WP-06 plugin)."
610
+ ),
611
+ )
612
+ _parser.add_argument("--home", help="Override home directory (test hook).")
613
+ _parser.add_argument("--profile", help="SLM_MCP_PROFILE to inject.")
614
+ _parser.add_argument(
615
+ "--list", action="store_true",
616
+ help="Print supported IDE ids with display names and exit.",
617
+ )
618
+ _args = _parser.parse_args()
619
+
620
+ if _args.list:
621
+ for _id, _desc in IDE_MATRIX.items():
622
+ _flag = "[OUT]" if not _desc.fmt else ""
623
+ print(f" {_id:22s} {_desc.display} {_flag}".rstrip())
624
+ sys.exit(0)
625
+
626
+ _home = Path(_args.home) if _args.home else None
627
+
628
+ # Resolve targets: "all" → every MCP-capable IDE (fmt != ""), else explicit list.
629
+ if "all" in _args.ide_ids:
630
+ _targets = [k for k, d in IDE_MATRIX.items() if d.fmt]
631
+ else:
632
+ _targets = [t for t in _args.ide_ids if t]
633
+
634
+ if not _targets:
635
+ print("No IDE ids given. Pass ide ids or 'all'. Use --list to see options.")
636
+ sys.exit(0)
637
+
638
+ _results = connect_many(_targets, home=_home, profile=_args.profile)
639
+ _ok = 0
640
+ _fail = 0
641
+ for _r in _results:
642
+ _err = _r.get("error")
643
+ _status = _r.get("mcp_config", "error")
644
+ if _err:
645
+ print(f" ERROR {_r['ide']}: {_err}", file=sys.stderr)
646
+ _fail += 1
647
+ else:
648
+ print(f" {_status.upper():9s} {_r['ide']} → {_r.get('mcp_path', '')}")
649
+ _ok += 1
650
+ print(f"\n{_ok} connected, {_fail} errors.")
651
+ sys.exit(0 if _fail == 0 else 1)
@@ -266,7 +266,18 @@ class BackupManager:
266
266
 
267
267
  A safety snapshot of the current state is taken first.
268
268
  """
269
- backup_path = self.backup_dir / filename
269
+ # Containment: filename must be a bare .db name inside backup_dir no
270
+ # path separators or traversal. Prevents restoring (and thus copying
271
+ # over memory.db) an arbitrary file the daemon user can read.
272
+ if (not filename or "/" in filename or "\\" in filename
273
+ or ".." in filename or not filename.endswith(".db")):
274
+ logger.error("Restore rejected: invalid backup filename: %r", filename)
275
+ return False
276
+ backup_dir = self.backup_dir.resolve()
277
+ backup_path = (self.backup_dir / filename).resolve()
278
+ if backup_path.parent != backup_dir:
279
+ logger.error("Restore rejected: path escapes backup dir: %r", filename)
280
+ return False
270
281
  if not backup_path.exists():
271
282
  logger.error("Backup not found: %s", filename)
272
283
  return False
@@ -14,8 +14,11 @@ import getpass
14
14
  import hashlib
15
15
  import hmac
16
16
  import json
17
+ import logging
17
18
  import os
18
19
  import secrets
20
+ import subprocess
21
+ import sys
19
22
  import time
20
23
  import uuid
21
24
  from dataclasses import asdict, dataclass
@@ -30,6 +33,8 @@ DAEMON_SERVICE = "superlocalmemory-daemon"
30
33
  _NAMESPACE_DOMAIN = b"superlocalmemory-daemon-namespace-v1\0"
31
34
  _CAPABILITY_DOMAIN = b"superlocalmemory-daemon-capability-v1\0"
32
35
 
36
+ logger = logging.getLogger(__name__)
37
+
33
38
 
34
39
  def _canonical_path(value: str | Path) -> Path:
35
40
  expanded = Path(value).expanduser().resolve(strict=False)
@@ -144,6 +149,40 @@ def descriptor_path(data_root: str | Path | None = None) -> Path:
144
149
  return root / "daemon.json"
145
150
 
146
151
 
152
+ def _restrict_to_owner(path: Path) -> None:
153
+ """Restrict a sensitive file to the current user on every platform.
154
+
155
+ ``daemon.json`` holds the capability token that authorizes write requests.
156
+ POSIX gets a 0600 chmod. On Windows chmod is a no-op, so use icacls to strip
157
+ inherited ACEs and grant the current user only — otherwise the token can be
158
+ read by other local accounts (privilege escalation on shared machines).
159
+ Files under %USERPROFILE% usually inherit user-only ACLs already; this is
160
+ defense-in-depth. Fail-soft — a hardening failure warns, never crashes.
161
+ """
162
+ if sys.platform == "win32":
163
+ try:
164
+ user = getpass.getuser()
165
+ subprocess.run(
166
+ ["icacls", str(path), "/inheritance:r"],
167
+ check=False, capture_output=True,
168
+ )
169
+ if user:
170
+ subprocess.run(
171
+ ["icacls", str(path), "/grant:r", f"{user}:F"],
172
+ check=False, capture_output=True,
173
+ )
174
+ except Exception as exc: # noqa: BLE001
175
+ logger.warning(
176
+ "could not restrict ACL on %s (%s); the capability token may be "
177
+ "readable by other local users", path, exc,
178
+ )
179
+ else:
180
+ try:
181
+ os.chmod(path, 0o600)
182
+ except OSError as exc:
183
+ logger.warning("could not chmod %s to 0600: %s", path, exc)
184
+
185
+
147
186
  def write_descriptor(
148
187
  descriptor: DaemonDescriptor,
149
188
  *,
@@ -162,10 +201,7 @@ def write_descriptor(
162
201
  stream.flush()
163
202
  os.fsync(stream.fileno())
164
203
  os.replace(temporary, destination)
165
- try:
166
- os.chmod(destination, 0o600)
167
- except OSError:
168
- pass
204
+ _restrict_to_owner(destination)
169
205
  finally:
170
206
  temporary.unlink(missing_ok=True)
171
207
  return destination