apsimo 1.3.0__py3-none-any.whl

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 (614) hide show
  1. apsimo/__init__.py +38 -0
  2. apsimo/__main__.py +6 -0
  3. apsimo/agent/__init__.py +6 -0
  4. apsimo/agent/client.py +276 -0
  5. apsimo/agent/models.py +46 -0
  6. apsimo/agents/__init__.py +20 -0
  7. apsimo/agents/models.py +264 -0
  8. apsimo/agents/store.py +861 -0
  9. apsimo/agents/websocket.py +522 -0
  10. apsimo/api/__init__.py +1 -0
  11. apsimo/api/auth_telemetry.py +287 -0
  12. apsimo/api/authority.py +1203 -0
  13. apsimo/api/contact_grants.py +347 -0
  14. apsimo/api/middleware.py +483 -0
  15. apsimo/api/routers/__init__.py +1 -0
  16. apsimo/api/routers/commitment_work.py +265 -0
  17. apsimo/api/routers/context_gate.py +123 -0
  18. apsimo/api/routers/executions.py +140 -0
  19. apsimo/api/routers/followup_plans.py +147 -0
  20. apsimo/api/routers/governed_actions.py +162 -0
  21. apsimo/api/routers/host.py +14473 -0
  22. apsimo/api/routers/initiative_work.py +115 -0
  23. apsimo/api/routers/mining.py +104 -0
  24. apsimo/api/routers/observations.py +110 -0
  25. apsimo/api/routers/social_state.py +225 -0
  26. apsimo/api/routers/task_queue.py +2715 -0
  27. apsimo/api/routers/temporal_followups.py +251 -0
  28. apsimo/api/routers/transport.py +110 -0
  29. apsimo/api/routers/transport_ingress_api.py +240 -0
  30. apsimo/api/schemas/__init__.py +1 -0
  31. apsimo/api/schemas/host.py +1949 -0
  32. apsimo/autonomy/cli.py +110 -0
  33. apsimo/autonomy/condition_worker.py +437 -0
  34. apsimo/autonomy/config.py +424 -0
  35. apsimo/autonomy/loop.py +4316 -0
  36. apsimo/autonomy/registry.py +339 -0
  37. apsimo/autonomy/scheduler.py +1822 -0
  38. apsimo/autonomy/synthesis.py +449 -0
  39. apsimo/backup.py +962 -0
  40. apsimo/beliefs/__init__.py +23 -0
  41. apsimo/beliefs/contradictions.py +109 -0
  42. apsimo/beliefs/decay.py +61 -0
  43. apsimo/beliefs/engine.py +479 -0
  44. apsimo/beliefs/models.py +67 -0
  45. apsimo/beliefs/promotion.py +41 -0
  46. apsimo/beliefs/resolve.py +58 -0
  47. apsimo/beliefs/source_claims.py +690 -0
  48. apsimo/beliefs/source_projection.py +883 -0
  49. apsimo/beliefs/source_time.py +208 -0
  50. apsimo/beliefs/store.py +133 -0
  51. apsimo/briefings/aggregators.py +824 -0
  52. apsimo/briefings/composer.py +420 -0
  53. apsimo/briefings/config.py +55 -0
  54. apsimo/briefings/delivery.py +439 -0
  55. apsimo/briefings/engagement.py +97 -0
  56. apsimo/briefings/engine.py +274 -0
  57. apsimo/briefings/enhancer.py +99 -0
  58. apsimo/briefings/models.py +183 -0
  59. apsimo/briefings/scheduler.py +382 -0
  60. apsimo/briefings/store.py +435 -0
  61. apsimo/chain/__init__.py +48 -0
  62. apsimo/chain/block.py +100 -0
  63. apsimo/chain/cli.py +704 -0
  64. apsimo/chain/genesis.py +443 -0
  65. apsimo/chain/identity.py +416 -0
  66. apsimo/chain/keys.py +1025 -0
  67. apsimo/chain/local_keys.py +187 -0
  68. apsimo/chain/manager.py +290 -0
  69. apsimo/chain/node.py +163 -0
  70. apsimo/chain/plugin_transactions.py +371 -0
  71. apsimo/chain/protocol.py +220 -0
  72. apsimo/chain/state_machine.py +676 -0
  73. apsimo/chain/storage.py +503 -0
  74. apsimo/chain/transactions.py +250 -0
  75. apsimo/chain/validation.py +397 -0
  76. apsimo/channels/__init__.py +1 -0
  77. apsimo/channels/manifest.py +31 -0
  78. apsimo/channels/migrations/001_channels_schema.sql +12 -0
  79. apsimo/channels/phone_gateways.py +42 -0
  80. apsimo/channels/presence.py +188 -0
  81. apsimo/channels/router.py +235 -0
  82. apsimo/channels/store.py +231 -0
  83. apsimo/cli.py +2688 -0
  84. apsimo/cognition/__init__.py +11 -0
  85. apsimo/cognition/charter.py +398 -0
  86. apsimo/cognition/drive_governance.py +3530 -0
  87. apsimo/cognition/evidence_pipeline.py +1627 -0
  88. apsimo/cognition/external_events.py +932 -0
  89. apsimo/cognition/goal_spine.py +3488 -0
  90. apsimo/cognition/introspection.py +214 -0
  91. apsimo/cognition/prompt.py +150 -0
  92. apsimo/cognition/runtime.py +108 -0
  93. apsimo/cognition/trigger.py +154 -0
  94. apsimo/commitments/__init__.py +18 -0
  95. apsimo/commitments/local_work.py +355 -0
  96. apsimo/commitments/store.py +1052 -0
  97. apsimo/commitments/work.py +91 -0
  98. apsimo/compat.py +53 -0
  99. apsimo/compression/__init__.py +467 -0
  100. apsimo/connectors/__init__.py +21 -0
  101. apsimo/connectors/base.py +152 -0
  102. apsimo/connectors/caldav_calendar.py +125 -0
  103. apsimo/connectors/fs_documents.py +85 -0
  104. apsimo/connectors/imap_email.py +138 -0
  105. apsimo/connectors/manager.py +218 -0
  106. apsimo/connectors/webhook_pull.py +88 -0
  107. apsimo/contacts/__init__.py +33 -0
  108. apsimo/contacts/comms.py +357 -0
  109. apsimo/contacts/config.py +79 -0
  110. apsimo/contacts/exporters/__init__.py +1 -0
  111. apsimo/contacts/exporters/vcard.py +71 -0
  112. apsimo/contacts/identity_links.py +251 -0
  113. apsimo/contacts/importer.py +280 -0
  114. apsimo/contacts/importers/__init__.py +1 -0
  115. apsimo/contacts/importers/batch.py +43 -0
  116. apsimo/contacts/importers/macos_contacts.py +101 -0
  117. apsimo/contacts/migrations/001_contacts_schema.sql +141 -0
  118. apsimo/contacts/migrations/002_trust_scopes.sql +36 -0
  119. apsimo/contacts/migrations/003_open_gateway_enum.sql +32 -0
  120. apsimo/contacts/migrations/004_contact_provision_operations.sql +18 -0
  121. apsimo/contacts/migrations/005_identity_links.sql +27 -0
  122. apsimo/contacts/models.py +308 -0
  123. apsimo/contacts/scoring.py +16 -0
  124. apsimo/contacts/store.py +1623 -0
  125. apsimo/contacts/transport_ingress.py +252 -0
  126. apsimo/contacts/world_bridge.py +314 -0
  127. apsimo/contextgate/__init__.py +69 -0
  128. apsimo/contextgate/chunker.py +169 -0
  129. apsimo/contextgate/estimate.py +54 -0
  130. apsimo/contextgate/gate.py +313 -0
  131. apsimo/contextgate/retrieve.py +115 -0
  132. apsimo/delivery/__init__.py +16 -0
  133. apsimo/delivery/bridge.py +1260 -0
  134. apsimo/delivery/channels.py +526 -0
  135. apsimo/delivery/classification.py +50 -0
  136. apsimo/delivery/rate_limiter.py +268 -0
  137. apsimo/delivery/reachout_policy.py +206 -0
  138. apsimo/directed/__init__.py +22 -0
  139. apsimo/directed/audit.py +167 -0
  140. apsimo/directed/intake.py +95 -0
  141. apsimo/directed/models.py +191 -0
  142. apsimo/directed/service.py +509 -0
  143. apsimo/directives/__init__.py +25 -0
  144. apsimo/directives/evidence.py +87 -0
  145. apsimo/directives/extractor.py +188 -0
  146. apsimo/directives/guard.py +364 -0
  147. apsimo/directives/models.py +206 -0
  148. apsimo/directives/service.py +372 -0
  149. apsimo/directives/store.py +167 -0
  150. apsimo/doctor.py +2173 -0
  151. apsimo/environment.py +43 -0
  152. apsimo/events/__init__.py +33 -0
  153. apsimo/events/broadcaster.py +98 -0
  154. apsimo/events/bus.py +217 -0
  155. apsimo/events/journal.py +863 -0
  156. apsimo/events/stream.py +131 -0
  157. apsimo/events/types.py +150 -0
  158. apsimo/execution_results.py +357 -0
  159. apsimo/feedback/__init__.py +5 -0
  160. apsimo/feedback/store.py +76 -0
  161. apsimo/feeds/__init__.py +19 -0
  162. apsimo/feeds/cli.py +84 -0
  163. apsimo/feeds/engine.py +437 -0
  164. apsimo/feeds/example-feed.yaml +77 -0
  165. apsimo/feeds/hermes_cron.py +126 -0
  166. apsimo/feeds/manager.py +235 -0
  167. apsimo/feeds/spec.py +250 -0
  168. apsimo/feeds/template.py +202 -0
  169. apsimo/gate/__init__.py +18 -0
  170. apsimo/gate/audit.py +61 -0
  171. apsimo/gate/communication_policy.py +166 -0
  172. apsimo/gate/config.py +72 -0
  173. apsimo/gate/context_provenance.py +170 -0
  174. apsimo/gate/env_risk.py +226 -0
  175. apsimo/gate/guard_audit.py +353 -0
  176. apsimo/gate/layers/__init__.py +1 -0
  177. apsimo/gate/layers/base.py +15 -0
  178. apsimo/gate/layers/l1_recipient.py +66 -0
  179. apsimo/gate/layers/l2_pii.py +134 -0
  180. apsimo/gate/layers/l3_cross_context.py +50 -0
  181. apsimo/gate/layers/l4_trust_tier.py +78 -0
  182. apsimo/gate/layers/l5_injection.py +199 -0
  183. apsimo/gate/layers/l6_review.py +86 -0
  184. apsimo/gate/layers/l7_delay.py +100 -0
  185. apsimo/gate/layers/tom2_epistemic.py +185 -0
  186. apsimo/gate/models.py +64 -0
  187. apsimo/gate/pending_dispatch.py +5 -0
  188. apsimo/gate/pipeline.py +206 -0
  189. apsimo/gate/rejection.py +259 -0
  190. apsimo/gate/response_guard.py +700 -0
  191. apsimo/gate/rulesets/injection_v1.yaml +51 -0
  192. apsimo/gate/surface_policy.py +189 -0
  193. apsimo/gate/taint.py +226 -0
  194. apsimo/genesis.json +9 -0
  195. apsimo/goals/__init__.py +100 -0
  196. apsimo/goals/config.py +38 -0
  197. apsimo/goals/decomposer.py +421 -0
  198. apsimo/goals/engine.py +617 -0
  199. apsimo/goals/inference.py +354 -0
  200. apsimo/goals/models.py +302 -0
  201. apsimo/goals/priority.py +270 -0
  202. apsimo/goals/queue_bridge.py +149 -0
  203. apsimo/goals/replan.py +450 -0
  204. apsimo/goals/schema.sql +89 -0
  205. apsimo/goals/store.py +692 -0
  206. apsimo/governed_actions.py +1708 -0
  207. apsimo/harness_integration/__init__.py +45 -0
  208. apsimo/harness_integration/context.py +41 -0
  209. apsimo/harness_integration/skills.py +231 -0
  210. apsimo/identity/__init__.py +26 -0
  211. apsimo/identity/participants.py +181 -0
  212. apsimo/identity/resolver.py +329 -0
  213. apsimo/identity_bootstrap/__init__.py +5 -0
  214. apsimo/identity_bootstrap/builder.py +208 -0
  215. apsimo/identity_bootstrap/corpus.py +443 -0
  216. apsimo/identity_bootstrap/models.py +54 -0
  217. apsimo/identity_bootstrap/runner.py +353 -0
  218. apsimo/identity_bootstrap/seeders/__init__.py +25 -0
  219. apsimo/identity_bootstrap/seeders/briefings.py +109 -0
  220. apsimo/identity_bootstrap/seeders/chain.py +57 -0
  221. apsimo/identity_bootstrap/seeders/goals.py +128 -0
  222. apsimo/identity_bootstrap/seeders/memory.py +191 -0
  223. apsimo/identity_bootstrap/seeders/neo4j_cognition.py +79 -0
  224. apsimo/identity_bootstrap/seeders/relationship.py +152 -0
  225. apsimo/identity_bootstrap/seeders/sessions.py +67 -0
  226. apsimo/identity_bootstrap/seeders/skills.py +92 -0
  227. apsimo/identity_bootstrap/seeders/task_queue.py +72 -0
  228. apsimo/identity_bootstrap/seeders/world_model.py +143 -0
  229. apsimo/identity_bootstrap/self_query.py +92 -0
  230. apsimo/identity_bootstrap/self_reflection.py +155 -0
  231. apsimo/identity_bootstrap/skill.py +37 -0
  232. apsimo/identity_bootstrap/verifier.py +436 -0
  233. apsimo/initiatives/__init__.py +20 -0
  234. apsimo/initiatives/action_registry.py +454 -0
  235. apsimo/initiatives/approval_authority.py +2105 -0
  236. apsimo/initiatives/approval_policy.py +123 -0
  237. apsimo/initiatives/assignment.py +263 -0
  238. apsimo/initiatives/backup_evidence.py +100 -0
  239. apsimo/initiatives/context_freshness.py +103 -0
  240. apsimo/initiatives/models.py +318 -0
  241. apsimo/initiatives/native_work.py +270 -0
  242. apsimo/initiatives/standing_approvals.py +232 -0
  243. apsimo/initiatives/store.py +1081 -0
  244. apsimo/initiatives/temporal_followup.py +410 -0
  245. apsimo/intelligence/__init__.py +1 -0
  246. apsimo/intelligence/cognition/__init__.py +24 -0
  247. apsimo/intelligence/cognition/gap_detector.py +148 -0
  248. apsimo/intelligence/cognition/metalearner.py +547 -0
  249. apsimo/intelligence/cognition/metrics_collector.py +217 -0
  250. apsimo/intelligence/cognition/performance_index.py +299 -0
  251. apsimo/intelligence/cognition/registry.py +192 -0
  252. apsimo/intelligence/cognition/strategy_adjuster.py +222 -0
  253. apsimo/intelligence/cognition/types.py +16 -0
  254. apsimo/intelligence/components/__init__.py +66 -0
  255. apsimo/intelligence/components/anomaly_detector.py +413 -0
  256. apsimo/intelligence/components/initiative_engine.py +2643 -0
  257. apsimo/intelligence/components/preference_learner.py +521 -0
  258. apsimo/intelligence/components/research_orchestrator.py +358 -0
  259. apsimo/intelligence/components/self_directed_thinker.py +221 -0
  260. apsimo/intelligence/components/self_reflector.py +252 -0
  261. apsimo/intelligence/components/session_continuity.py +154 -0
  262. apsimo/intelligence/components/task_planner.py +320 -0
  263. apsimo/intelligence/components/tool_learner.py +217 -0
  264. apsimo/intelligence/graph/__init__.py +79 -0
  265. apsimo/intelligence/graph/client.py +2483 -0
  266. apsimo/intelligence/graph/consolidator.py +405 -0
  267. apsimo/intelligence/graph/distiller.py +312 -0
  268. apsimo/intelligence/graph/migrations.py +129 -0
  269. apsimo/intelligence/graph/queries.py +248 -0
  270. apsimo/intelligence/graph/recall.py +281 -0
  271. apsimo/intelligence/graph/reconciler.py +144 -0
  272. apsimo/intelligence/graph/schema.py +337 -0
  273. apsimo/intelligence/graph/selection.py +252 -0
  274. apsimo/intelligence/learning/__init__.py +17 -0
  275. apsimo/intelligence/learning/continuous_learner.py +245 -0
  276. apsimo/intelligence/learning/feedback_store.py +321 -0
  277. apsimo/intelligence/mind_model/__init__.py +1 -0
  278. apsimo/intelligence/mind_model/graph_baseline.py +136 -0
  279. apsimo/intelligence/mind_model/signal_collector.py +361 -0
  280. apsimo/intelligence/relationships/__init__.py +11 -0
  281. apsimo/intelligence/relationships/profiler.py +389 -0
  282. apsimo/intelligence/relationships/scorer.py +560 -0
  283. apsimo/intelligence/relationships/signal_floor.py +66 -0
  284. apsimo/intelligence/relationships/trust_tiers.py +300 -0
  285. apsimo/intelligence/synthesis/__init__.py +40 -0
  286. apsimo/intelligence/synthesis/connection_discoverer.py +379 -0
  287. apsimo/intelligence/synthesis/cross_domain_analyzer.py +287 -0
  288. apsimo/intelligence/synthesis/insight_deliverer.py +171 -0
  289. apsimo/intelligence/synthesis/insight_store.py +79 -0
  290. apsimo/intelligence/synthesis/insight_validator.py +183 -0
  291. apsimo/intelligence/synthesis/novelty_scorer.py +267 -0
  292. apsimo/intelligence/turn_middleware/__init__.py +15 -0
  293. apsimo/intelligence/turn_middleware/memory_sync.py +119 -0
  294. apsimo/mcp/__init__.py +41 -0
  295. apsimo/mcp/__main__.py +6 -0
  296. apsimo/mcp/config.py +287 -0
  297. apsimo/mcp/server.py +501 -0
  298. apsimo/migrations.py +187 -0
  299. apsimo/mining/__init__.py +27 -0
  300. apsimo/mining/corpus.py +239 -0
  301. apsimo/mining/escalations.py +289 -0
  302. apsimo/mining/models.py +169 -0
  303. apsimo/mining/store.py +210 -0
  304. apsimo/models/__init__.py +30 -0
  305. apsimo/models/memory.py +80 -0
  306. apsimo/models/mesh.py +72 -0
  307. apsimo/models/person.py +104 -0
  308. apsimo/models/signal.py +108 -0
  309. apsimo/observations/__init__.py +15 -0
  310. apsimo/observations/store.py +277 -0
  311. apsimo/patterns/__init__.py +6 -0
  312. apsimo/patterns/extract.py +187 -0
  313. apsimo/patterns/store.py +227 -0
  314. apsimo/persona/__init__.py +1 -0
  315. apsimo/persona/engine.py +611 -0
  316. apsimo/persona/manifest.py +140 -0
  317. apsimo/projects/__init__.py +28 -0
  318. apsimo/projects/engine.py +1681 -0
  319. apsimo/projects/event_outbox.py +188 -0
  320. apsimo/projects/models.py +216 -0
  321. apsimo/projects/planner.py +181 -0
  322. apsimo/projects/store.py +1446 -0
  323. apsimo/proposals/__init__.py +12 -0
  324. apsimo/proposals/engine.py +114 -0
  325. apsimo/proposals/models.py +207 -0
  326. apsimo/qualification/__init__.py +1 -0
  327. apsimo/qualification/cases.py +75 -0
  328. apsimo/qualification/cli.py +51 -0
  329. apsimo/qualification/memory_cases.py +209 -0
  330. apsimo/qualification/records.py +92 -0
  331. apsimo/qualification/report.py +87 -0
  332. apsimo/qualification/runner.py +311 -0
  333. apsimo/qualification/structured_cases.py +131 -0
  334. apsimo/reasoning/__init__.py +13 -0
  335. apsimo/reasoning/executor.py +506 -0
  336. apsimo/reasoning/loop.py +373 -0
  337. apsimo/reasoning/native_tools/__init__.py +16 -0
  338. apsimo/reasoning/native_tools/calculate.py +141 -0
  339. apsimo/reasoning/native_tools/file_ops.py +150 -0
  340. apsimo/reasoning/native_tools/web_search.py +49 -0
  341. apsimo/reasoning/tool_policy.py +182 -0
  342. apsimo/redact/__init__.py +176 -0
  343. apsimo/repos/__init__.py +5 -0
  344. apsimo/repos/mirrors.py +204 -0
  345. apsimo/research/__init__.py +41 -0
  346. apsimo/research/artifact.py +482 -0
  347. apsimo/research/gatherer.py +387 -0
  348. apsimo/research/pipeline.py +513 -0
  349. apsimo/research/search/__init__.py +7 -0
  350. apsimo/research/search/base.py +41 -0
  351. apsimo/research/search/brave.py +59 -0
  352. apsimo/research/search/cache.py +51 -0
  353. apsimo/research/search/duckduckgo.py +103 -0
  354. apsimo/research/search/orchestrator.py +119 -0
  355. apsimo/research/search/serpapi.py +59 -0
  356. apsimo/research/search/tavily.py +59 -0
  357. apsimo/research/synthesizer.py +309 -0
  358. apsimo/router/__init__.py +30 -0
  359. apsimo/router/complexity_scorer.py +148 -0
  360. apsimo/router/endpoints.py +153 -0
  361. apsimo/router/fallback.py +58 -0
  362. apsimo/router/functions.py +243 -0
  363. apsimo/router/native_policy.py +52 -0
  364. apsimo/router/router.py +762 -0
  365. apsimo/router/self_learning.py +174 -0
  366. apsimo/router/tiers.py +677 -0
  367. apsimo/sandbox/__init__.py +21 -0
  368. apsimo/sandbox/backend.py +195 -0
  369. apsimo/sandbox/manager.py +173 -0
  370. apsimo/scope_bounds.py +7 -0
  371. apsimo/secrets/__init__.py +6 -0
  372. apsimo/secrets/backends/__init__.py +8 -0
  373. apsimo/secrets/backends/base.py +42 -0
  374. apsimo/secrets/backends/env.py +110 -0
  375. apsimo/secrets/backends/keyring.py +72 -0
  376. apsimo/secrets/backends/onepassword.py +232 -0
  377. apsimo/secrets/cli.py +191 -0
  378. apsimo/secrets/manager.py +160 -0
  379. apsimo/secrets/migration.py +101 -0
  380. apsimo/secrets/types.py +98 -0
  381. apsimo/seed.py +41 -0
  382. apsimo/self_model/__init__.py +37 -0
  383. apsimo/self_model/appraisals.py +673 -0
  384. apsimo/self_model/benchmark.py +1314 -0
  385. apsimo/self_model/brief.py +40 -0
  386. apsimo/self_model/event_concerns.py +1128 -0
  387. apsimo/self_model/execution_forecasts.py +353 -0
  388. apsimo/self_model/expectations.py +1595 -0
  389. apsimo/self_model/experiments.py +1150 -0
  390. apsimo/self_model/journal.py +148 -0
  391. apsimo/self_model/judgments.py +705 -0
  392. apsimo/self_model/native_outcomes.py +55 -0
  393. apsimo/self_model/params.py +220 -0
  394. apsimo/self_model/perspective.py +246 -0
  395. apsimo/self_model/reconcile.py +183 -0
  396. apsimo/self_model/reply_forecasts.py +381 -0
  397. apsimo/self_model/runtime_forecasts.py +296 -0
  398. apsimo/self_model/runtime_models.py +67 -0
  399. apsimo/self_model/settlement.py +207 -0
  400. apsimo/self_model/situation.py +1731 -0
  401. apsimo/self_model/store.py +883 -0
  402. apsimo/self_model/supervised.py +137 -0
  403. apsimo/self_model/thinker.py +99 -0
  404. apsimo/self_model/trust.py +388 -0
  405. apsimo/self_model/workspace.py +2388 -0
  406. apsimo/server.py +4197 -0
  407. apsimo/services/__init__.py +1 -0
  408. apsimo/services/agent_bridge.py +474 -0
  409. apsimo/services/initiative_executor.py +914 -0
  410. apsimo/services/instance.py +297 -0
  411. apsimo/sessions/__init__.py +22 -0
  412. apsimo/sessions/config.py +13 -0
  413. apsimo/sessions/context_loader.py +88 -0
  414. apsimo/sessions/federation_session.py +75 -0
  415. apsimo/sessions/isolated_session.py +98 -0
  416. apsimo/sessions/reports.py +84 -0
  417. apsimo/sessions/store.py +148 -0
  418. apsimo/setup.py +2818 -0
  419. apsimo/setup_hermes.py +879 -0
  420. apsimo/setup_local_work.py +218 -0
  421. apsimo/setup_native_goals.py +134 -0
  422. apsimo/setup_native_reviews.py +115 -0
  423. apsimo/skills/__init__.py +10 -0
  424. apsimo/skills/base.py +108 -0
  425. apsimo/skills/budget.py +28 -0
  426. apsimo/skills/executor.py +493 -0
  427. apsimo/skills/executors/__init__.py +1 -0
  428. apsimo/skills/executors/behavioral_correction.py +75 -0
  429. apsimo/skills/executors/capability_gap.py +38 -0
  430. apsimo/skills/executors/data_quality.py +163 -0
  431. apsimo/skills/executors/knowledge_acquisition.py +41 -0
  432. apsimo/skills/executors/operational_hygiene.py +185 -0
  433. apsimo/skills/executors/subsystem_health.py +169 -0
  434. apsimo/skills/hermes_export.py +431 -0
  435. apsimo/skills/index.py +123 -0
  436. apsimo/skills/learning/__init__.py +21 -0
  437. apsimo/skills/learning/novelty_detector.py +206 -0
  438. apsimo/skills/learning/pattern_extractor.py +199 -0
  439. apsimo/skills/learning/triggers.py +159 -0
  440. apsimo/skills/loader.py +246 -0
  441. apsimo/skills/migrations/002_progressive_loading.sql +6 -0
  442. apsimo/skills/migrations/backfill_triggers.py +20 -0
  443. apsimo/skills/models.py +202 -0
  444. apsimo/skills/packager.py +128 -0
  445. apsimo/skills/protocols.py +70 -0
  446. apsimo/skills/registry.py +191 -0
  447. apsimo/skills/runtime.py +58 -0
  448. apsimo/skills/sandbox_runner.py +229 -0
  449. apsimo/skills/scheduler.py +129 -0
  450. apsimo/skills/schema.py +79 -0
  451. apsimo/skills/security/__init__.py +12 -0
  452. apsimo/skills/security/guards.py +53 -0
  453. apsimo/skills/security/scanner.py +223 -0
  454. apsimo/skills_memory/__init__.py +26 -0
  455. apsimo/skills_memory/distill.py +159 -0
  456. apsimo/skills_memory/models.py +85 -0
  457. apsimo/skills_memory/retrieve.py +62 -0
  458. apsimo/skills_memory/store.py +172 -0
  459. apsimo/surprise/__init__.py +6 -0
  460. apsimo/surprise/accumulation.py +57 -0
  461. apsimo/surprise/scorer.py +102 -0
  462. apsimo/surprise/store.py +203 -0
  463. apsimo/task_queue/__init__.py +69 -0
  464. apsimo/task_queue/action_receipts.py +148 -0
  465. apsimo/task_queue/approval_relay_canary.py +108 -0
  466. apsimo/task_queue/config.py +85 -0
  467. apsimo/task_queue/contract.py +361 -0
  468. apsimo/task_queue/events.py +130 -0
  469. apsimo/task_queue/governor.py +1031 -0
  470. apsimo/task_queue/handlers/__init__.py +16 -0
  471. apsimo/task_queue/handlers/base.py +37 -0
  472. apsimo/task_queue/handlers/inference.py +640 -0
  473. apsimo/task_queue/handlers/monitoring.py +116 -0
  474. apsimo/task_queue/handlers/registry.py +75 -0
  475. apsimo/task_queue/handlers/subtask_handler.py +173 -0
  476. apsimo/task_queue/handlers/system_maintenance.py +147 -0
  477. apsimo/task_queue/mesh_integration.py +111 -0
  478. apsimo/task_queue/models.py +317 -0
  479. apsimo/task_queue/queue_manager.py +8286 -0
  480. apsimo/task_queue/routing.py +287 -0
  481. apsimo/task_queue/scheduler.py +252 -0
  482. apsimo/task_queue/schema.sql +197 -0
  483. apsimo/task_queue/work_control.py +342 -0
  484. apsimo/task_queue/worker.py +993 -0
  485. apsimo/telemetry.py +145 -0
  486. apsimo/tom/__init__.py +6 -0
  487. apsimo/tom/affect.py +387 -0
  488. apsimo/tom/approvals.py +171 -0
  489. apsimo/tom/arcs.py +896 -0
  490. apsimo/tom/asymmetry.py +131 -0
  491. apsimo/tom/eligibility.py +248 -0
  492. apsimo/tom/engagement.py +214 -0
  493. apsimo/tom/exposure.py +214 -0
  494. apsimo/tom/extractor.py +306 -0
  495. apsimo/tom/fact_adapters.py +144 -0
  496. apsimo/tom/facts.py +326 -0
  497. apsimo/tom/integration.py +592 -0
  498. apsimo/tom/leveled.py +118 -0
  499. apsimo/tom/levels.py +247 -0
  500. apsimo/tom/recipient_audit.py +995 -0
  501. apsimo/tom/recipient_simulator.py +593 -0
  502. apsimo/tom/source_lineage.py +93 -0
  503. apsimo/tom/tom2.py +277 -0
  504. apsimo/tom/visibility.py +559 -0
  505. apsimo/tom/visibility_store.py +414 -0
  506. apsimo/tools/__init__.py +0 -0
  507. apsimo/tools/definitions.py +740 -0
  508. apsimo/tools/handlers.py +943 -0
  509. apsimo/toolsmith/__init__.py +26 -0
  510. apsimo/toolsmith/authority.py +166 -0
  511. apsimo/toolsmith/engine.py +559 -0
  512. apsimo/toolsmith/integrity.py +100 -0
  513. apsimo/toolsmith/miner.py +145 -0
  514. apsimo/toolsmith/policy.py +110 -0
  515. apsimo/toolsmith/registry.py +635 -0
  516. apsimo/turns/__init__.py +17 -0
  517. apsimo/turns/audio.py +134 -0
  518. apsimo/turns/documents.py +235 -0
  519. apsimo/turns/executions.py +486 -0
  520. apsimo/turns/hermes_history.py +245 -0
  521. apsimo/turns/hermes_kanban.py +268 -0
  522. apsimo/turns/hermes_work.py +96 -0
  523. apsimo/turns/idempotency.py +752 -0
  524. apsimo/turns/local_work.py +115 -0
  525. apsimo/turns/media.py +581 -0
  526. apsimo/turns/reported_workers.py +196 -0
  527. apsimo/turns/source_annotations.py +283 -0
  528. apsimo/turns/source_attribution.py +154 -0
  529. apsimo/turns/source_read.py +351 -0
  530. apsimo/turns/source_vectors.py +263 -0
  531. apsimo/turns/video.py +210 -0
  532. apsimo/util/autonomy_preset.py +220 -0
  533. apsimo/util/instance.py +92 -0
  534. apsimo/util/model_output.py +25 -0
  535. apsimo/util/quiet_hours.py +27 -0
  536. apsimo/util/session_safety.py +37 -0
  537. apsimo/util/temporal.py +343 -0
  538. apsimo/vector/__init__.py +75 -0
  539. apsimo/vector/backfill.py +171 -0
  540. apsimo/vector/caption.py +114 -0
  541. apsimo/vector/collections.py +51 -0
  542. apsimo/vector/config.py +102 -0
  543. apsimo/vector/embedder.py +670 -0
  544. apsimo/vector/image_preprocess.py +406 -0
  545. apsimo/vector/image_store.py +296 -0
  546. apsimo/vector/indexes.py +162 -0
  547. apsimo/vector/migrate.py +334 -0
  548. apsimo/vector/multimodal_provider.py +417 -0
  549. apsimo/vector/multimodal_types.py +87 -0
  550. apsimo/vector/openai_provider.py +119 -0
  551. apsimo/vector/query.py +49 -0
  552. apsimo/vector/reranker.py +565 -0
  553. apsimo/vector/safety_image.py +159 -0
  554. apsimo/vector/scanner.py +197 -0
  555. apsimo/vector/setup.py +289 -0
  556. apsimo/vector/store.py +533 -0
  557. apsimo/vector/tiers.py +263 -0
  558. apsimo/work_orders.py +925 -0
  559. apsimo/workers/__init__.py +21 -0
  560. apsimo/workers/agent_bridge.py +640 -0
  561. apsimo/workers/colony_worker.py +382 -0
  562. apsimo/workers/queue_worker.py +441 -0
  563. apsimo/workers/skills_sync.py +152 -0
  564. apsimo/world_model/__init__.py +71 -0
  565. apsimo/world_model/causal_maintenance.py +131 -0
  566. apsimo/world_model/causal_policy.py +43 -0
  567. apsimo/world_model/causal_query.py +125 -0
  568. apsimo/world_model/confidence.py +54 -0
  569. apsimo/world_model/config.py +64 -0
  570. apsimo/world_model/constants.py +97 -0
  571. apsimo/world_model/entities.py +145 -0
  572. apsimo/world_model/expectation_resolvers.py +177 -0
  573. apsimo/world_model/extraction/__init__.py +7 -0
  574. apsimo/world_model/extraction/base.py +62 -0
  575. apsimo/world_model/extraction/conversation_extractor.py +262 -0
  576. apsimo/world_model/extraction/detector.py +74 -0
  577. apsimo/world_model/extraction/document_extractor.py +78 -0
  578. apsimo/world_model/extraction/formats/__init__.py +24 -0
  579. apsimo/world_model/extraction/formats/csv_fmt.py +68 -0
  580. apsimo/world_model/extraction/formats/html_fmt.py +72 -0
  581. apsimo/world_model/extraction/formats/json_fmt.py +68 -0
  582. apsimo/world_model/extraction/formats/pdf.py +43 -0
  583. apsimo/world_model/extraction/formats/text.py +27 -0
  584. apsimo/world_model/extraction/llm_extractor.py +164 -0
  585. apsimo/world_model/extraction/pipeline.py +73 -0
  586. apsimo/world_model/integrations/__init__.py +5 -0
  587. apsimo/world_model/integrations/mind_model_bridge.py +115 -0
  588. apsimo/world_model/integrations/social_intel_bridge.py +120 -0
  589. apsimo/world_model/jobs/__init__.py +4 -0
  590. apsimo/world_model/jobs/extraction_job.py +168 -0
  591. apsimo/world_model/llm_extract.py +572 -0
  592. apsimo/world_model/neo4j/__init__.py +5 -0
  593. apsimo/world_model/neo4j/backend.py +654 -0
  594. apsimo/world_model/observations.py +155 -0
  595. apsimo/world_model/populator.py +307 -0
  596. apsimo/world_model/postgres/__init__.py +1 -0
  597. apsimo/world_model/postgres/backend.py +683 -0
  598. apsimo/world_model/relationships.py +25 -0
  599. apsimo/world_model/resolution/__init__.py +13 -0
  600. apsimo/world_model/resolution/entity_resolver.py +232 -0
  601. apsimo/world_model/resolution/merge_audit.py +16 -0
  602. apsimo/world_model/resolution/merge_workflow.py +117 -0
  603. apsimo/world_model/source_reports.py +121 -0
  604. apsimo/world_model/sqlite/__init__.py +4 -0
  605. apsimo/world_model/sqlite/backend.py +855 -0
  606. apsimo/world_model/sqlite/schema.sql +132 -0
  607. apsimo/world_model/store.py +545 -0
  608. apsimo-1.3.0.dist-info/METADATA +78 -0
  609. apsimo-1.3.0.dist-info/RECORD +614 -0
  610. apsimo-1.3.0.dist-info/WHEEL +5 -0
  611. apsimo-1.3.0.dist-info/entry_points.txt +11 -0
  612. apsimo-1.3.0.dist-info/licenses/LICENSE +21 -0
  613. apsimo-1.3.0.dist-info/top_level.txt +2 -0
  614. colony_sidecar/__init__.py +4 -0
@@ -0,0 +1,187 @@
1
+ """Colony Local Key Manager — PEM-based Ed25519 signing for Phase 1/2.
2
+
3
+ Provides the same interface as ColonyKeyManager (sign, public_key_hex, rotate)
4
+ but stores the key as a simple PEM file. Designed to be swapped out for the
5
+ Shamir-backed ColonyKeyManager when Phase 3/4/5 ship.
6
+
7
+ Key storage:
8
+ {state_dir}/colony-keys/
9
+ private.pem — Ed25519 private key (optionally encrypted)
10
+ public.pem — Ed25519 public key (always readable)
11
+
12
+ Migration path:
13
+ Phase 1/2: LocalKeyManager (PEM, optional passphrase)
14
+ Phase 3: LocalKeyManager with passphrase (colony key set-passphrase)
15
+ Phase 4/5: ColonyKeyManager (Shamir-split, colony key migrate-to-shamir)
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import logging
21
+ import os
22
+ from pathlib import Path
23
+ from typing import Callable, Optional
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class LocalKeyManager:
29
+ """PEM-based Ed25519 key manager for single-node Colony deployments."""
30
+
31
+ def __init__(
32
+ self,
33
+ keys_dir: str | Path,
34
+ colony_id: str,
35
+ passphrase: Optional[bytes] = None,
36
+ ) -> None:
37
+ self._keys_dir = Path(keys_dir)
38
+ self.colony_id = colony_id
39
+ self._passphrase = passphrase
40
+ self._keys_dir.mkdir(parents=True, exist_ok=True)
41
+
42
+ # ------------------------------------------------------------------
43
+ # Public interface (matches ColonyKeyManager)
44
+ # ------------------------------------------------------------------
45
+
46
+ def sign(self, payload: bytes) -> str:
47
+ """Sign payload bytes, returning hex-encoded Ed25519 signature."""
48
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
49
+
50
+ private_key = self._load_private_key()
51
+ signature = private_key.sign(payload)
52
+ return signature.hex()
53
+
54
+ def public_key_hex(self) -> str:
55
+ """Return the hex-encoded raw Ed25519 public key."""
56
+ from cryptography.hazmat.primitives import serialization
57
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
58
+
59
+ private_key = self._load_private_key()
60
+ pub = private_key.public_key().public_bytes(
61
+ encoding=serialization.Encoding.Raw,
62
+ format=serialization.PublicFormat.Raw,
63
+ )
64
+ return pub.hex()
65
+
66
+ def public_key_pem(self) -> str:
67
+ """Return the PEM-encoded public key."""
68
+ pub_path = self._keys_dir / "public.pem"
69
+ if pub_path.exists():
70
+ return pub_path.read_text()
71
+ # Generate from private key
72
+ from cryptography.hazmat.primitives import serialization
73
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
74
+
75
+ private_key = self._load_private_key()
76
+ pem = private_key.public_key().public_bytes(
77
+ encoding=serialization.Encoding.PEM,
78
+ format=serialization.PublicFormat.SubjectPublicKeyInfo,
79
+ )
80
+ pub_path.write_bytes(pem)
81
+ return pem.decode()
82
+
83
+ def rotate(self, new_passphrase: Optional[bytes] = None) -> None:
84
+ """Generate a new Ed25519 keypair, replacing the existing one.
85
+
86
+ The colony_id does NOT change — only the signing key rotates.
87
+ """
88
+ from cryptography.hazmat.primitives import serialization
89
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
90
+
91
+ # Backup old key
92
+ priv_path = self._keys_dir / "private.pem"
93
+ if priv_path.exists():
94
+ backup = self._keys_dir / "private.pem.backup"
95
+ backup.write_bytes(priv_path.read_bytes())
96
+
97
+ # Generate new keypair
98
+ private_key = Ed25519PrivateKey.generate()
99
+ self._save_keypair(private_key, new_passphrase or self._passphrase)
100
+ logger.info("Key rotated for colony %s", self.colony_id)
101
+
102
+ def set_passphrase(self, new_passphrase: bytes) -> None:
103
+ """Re-encrypt the private key with a new passphrase."""
104
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
105
+
106
+ private_key = self._load_private_key()
107
+ self._save_keypair(private_key, new_passphrase)
108
+ self._passphrase = new_passphrase
109
+ logger.info("Passphrase updated for colony %s", self.colony_id)
110
+
111
+ @property
112
+ def has_keys(self) -> bool:
113
+ """Check if keypair exists on disk."""
114
+ return (self._keys_dir / "private.pem").exists()
115
+
116
+ # ------------------------------------------------------------------
117
+ # Internal
118
+ # ------------------------------------------------------------------
119
+
120
+ def _load_private_key(self):
121
+ from cryptography.hazmat.primitives import serialization
122
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
123
+
124
+ priv_path = self._keys_dir / "private.pem"
125
+ if not priv_path.exists():
126
+ raise FileNotFoundError(
127
+ f"No private key found at {priv_path}. Run 'colony init' or 'colony key generate'."
128
+ )
129
+
130
+ pem_data = priv_path.read_bytes()
131
+ try:
132
+ return serialization.load_pem_private_key(
133
+ pem_data,
134
+ password=self._passphrase,
135
+ )
136
+ except (ValueError, TypeError):
137
+ # Wrong passphrase or unencrypted key with passphrase given
138
+ if self._passphrase:
139
+ raise ValueError("Wrong passphrase for private key") from None
140
+ # Try without passphrase
141
+ return serialization.load_pem_private_key(pem_data, password=None)
142
+
143
+ def _save_keypair(self, private_key, passphrase: Optional[bytes] = None) -> None:
144
+ from cryptography.hazmat.primitives import serialization
145
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
146
+
147
+ # Save private key
148
+ encryption: Any = serialization.NoEncryption()
149
+ if passphrase:
150
+ encryption = serialization.BestAvailableEncryption(passphrase)
151
+
152
+ priv_pem = private_key.private_bytes(
153
+ encoding=serialization.Encoding.PEM,
154
+ format=serialization.PrivateFormat.PKCS8,
155
+ encryption_algorithm=encryption,
156
+ )
157
+ (self._keys_dir / "private.pem").write_bytes(priv_pem)
158
+
159
+ # Save public key
160
+ pub_pem = private_key.public_key().public_bytes(
161
+ encoding=serialization.Encoding.PEM,
162
+ format=serialization.PublicFormat.SubjectPublicKeyInfo,
163
+ )
164
+ (self._keys_dir / "public.pem").write_bytes(pub_pem)
165
+
166
+ # Restrict permissions
167
+ os.chmod(self._keys_dir / "private.pem", 0o600)
168
+
169
+ @classmethod
170
+ def generate(
171
+ cls,
172
+ keys_dir: str | Path,
173
+ colony_id: str,
174
+ passphrase: Optional[bytes] = None,
175
+ ) -> "LocalKeyManager":
176
+ """Generate a new Ed25519 keypair and return a LocalKeyManager."""
177
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
178
+
179
+ mgr = cls(keys_dir=keys_dir, colony_id=colony_id, passphrase=passphrase)
180
+ private_key = Ed25519PrivateKey.generate()
181
+ mgr._save_keypair(private_key, passphrase)
182
+ logger.info("Generated new Ed25519 keypair for colony %s", colony_id)
183
+ return mgr
184
+
185
+
186
+ # Fix type annotation for Python 3.11 compat
187
+ from typing import Any
@@ -0,0 +1,290 @@
1
+ """ChainManager — main coordinator tying all chain components together.
2
+
3
+ Responsibilities:
4
+ - Owns the ChainStore, ChainStateMachine, and Mempool
5
+ - Chain sync protocol (pull blocks from Sentinels)
6
+ - New block announcement broadcasting
7
+ - Transaction submission and nonce tracking
8
+ - Integration hook for the federation module
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import asyncio
14
+ import json
15
+ import logging
16
+ import os
17
+ import sqlite3
18
+ from dataclasses import dataclass
19
+ from datetime import datetime, timezone
20
+ from pathlib import Path
21
+ from typing import Any, Callable, Coroutine, Optional
22
+
23
+ from apsimo import get_state_dir
24
+
25
+ from .block import Block
26
+ from .state_machine import ChainStateMachine
27
+ from .storage import ChainStore
28
+ from .transactions import ChainState, Transaction, TxType
29
+ from .validation import BlockValidator, TransactionValidator, ValidationResult
30
+ from .protocol import (
31
+ ChainAnnouncement,
32
+ ChainStateResponse,
33
+ ChainSyncRequest,
34
+ ChainSyncResponse,
35
+ TxSubmitResponse,
36
+ )
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+ _CHECKPOINT_INTERVAL = 100
41
+
42
+ _chain_manager_instance: Optional["ChainManager"] = None
43
+
44
+
45
+ @dataclass
46
+ class SubmitResult:
47
+ accepted: bool
48
+ tx_id: str
49
+ reason: str = ""
50
+
51
+
52
+ class ChainManager:
53
+ """Main coordinator for ColonyChain.
54
+
55
+ Manages the local chain replica, handles sync with Sentinels,
56
+ validates and queues transactions, and broadcasts new blocks.
57
+ """
58
+
59
+ def __init__(
60
+ self,
61
+ db_path: Path | str,
62
+ colony_id: str,
63
+ send_message: Callable[[str, dict[str, Any]], Coroutine] | None = None,
64
+ ) -> None:
65
+ self.colony_id = colony_id
66
+ self.store = ChainStore(db_path)
67
+ self.state_machine = ChainStateMachine(self.store)
68
+ self._send_message = send_message
69
+ self._state: ChainState | None = None
70
+ self._state_lock = asyncio.Lock()
71
+ self._block_validator = BlockValidator()
72
+
73
+ # ── State access ────────────────────────────────────────────────────────
74
+
75
+ async def get_state(self) -> ChainState:
76
+ """Return current chain state (thread-safe cached)."""
77
+ async with self._state_lock:
78
+ if self._state is None:
79
+ self._state = self.state_machine.get_current_state()
80
+ return self._state
81
+
82
+ def _invalidate_state(self) -> None:
83
+ self._state = None
84
+
85
+ # ── Transaction submission ───────────────────────────────────────────────
86
+
87
+ async def submit_transaction(self, tx: Transaction) -> SubmitResult:
88
+ """Validate and add a transaction to the mempool."""
89
+ state = await self.get_state()
90
+
91
+ # Nonce check (against persisted chain)
92
+ if tx.from_colony_id != "system":
93
+ persisted_nonce = self.store.get_nonce(tx.from_colony_id)
94
+ if tx.nonce <= persisted_nonce:
95
+ return SubmitResult(
96
+ accepted=False,
97
+ tx_id=tx.tx_id,
98
+ reason=f"nonce {tx.nonce} <= last accepted {persisted_nonce}",
99
+ )
100
+
101
+ validator = TransactionValidator(state, chain_store=self.store)
102
+ result = validator.validate(tx)
103
+ if not result.ok:
104
+ return SubmitResult(accepted=False, tx_id=tx.tx_id, reason=result.reason)
105
+
106
+ self.store.add_to_mempool(tx)
107
+ return SubmitResult(accepted=True, tx_id=tx.tx_id)
108
+
109
+ # ── Block commitment ─────────────────────────────────────────────────────
110
+
111
+ async def commit_block(self, block: Block) -> None:
112
+ """Commit a block to the chain and update state."""
113
+ latest = self.store.get_latest_block()
114
+ bv = self._block_validator
115
+ result = bv.validate_block(block, latest)
116
+ if not result.ok:
117
+ raise ValueError(f"Block validation failed: {result.reason}")
118
+
119
+ self.store.append_block(block)
120
+ self._invalidate_state()
121
+
122
+ # Save checkpoint every 100 blocks
123
+ if block.index > 0 and block.index % _CHECKPOINT_INTERVAL == 0:
124
+ state = await self.get_state()
125
+ self.store.save_checkpoint(state, block.index)
126
+
127
+ # Announce to peers
128
+ announcement = ChainAnnouncement(
129
+ block_index=block.index,
130
+ block_hash=block.block_hash,
131
+ producer_id=block.producer_id,
132
+ tx_count=len(block.transactions),
133
+ )
134
+ await self._broadcast(announcement.to_dict())
135
+
136
+ logger.info(
137
+ "Block %d committed (%d txs, hash %s…)",
138
+ block.index,
139
+ len(block.transactions),
140
+ block.block_hash[:16],
141
+ )
142
+
143
+ # ── Chain sync ───────────────────────────────────────────────────────────
144
+
145
+ async def sync_from_sentinel(
146
+ self,
147
+ sentinel_id: str,
148
+ from_height: int | None = None,
149
+ ) -> int:
150
+ """Pull blocks from a Sentinel to catch up. Returns blocks added."""
151
+ if from_height is None:
152
+ from_height = self.store.get_height() + 1
153
+
154
+ req = ChainSyncRequest(
155
+ from_index=from_height,
156
+ to_index=from_height + 499, # batch up to 500 blocks
157
+ requester_id=self.colony_id,
158
+ )
159
+ if self._send_message:
160
+ await self._send_message(sentinel_id, req.to_dict())
161
+ return 0
162
+
163
+ async def handle_chain_sync_response(self, data: dict[str, Any]) -> int:
164
+ """Process a CHAIN_SYNC_RESPONSE message. Returns blocks committed."""
165
+ blocks_data = data.get("blocks", [])
166
+ committed = 0
167
+ for block_dict in blocks_data:
168
+ block = Block.from_dict(block_dict)
169
+ try:
170
+ await self.commit_block(block)
171
+ committed += 1
172
+ except ValueError as exc:
173
+ logger.warning("Skipping block %d: %s", block.index, exc)
174
+ break
175
+ return committed
176
+
177
+ async def handle_chain_announce(self, data: dict[str, Any]) -> None:
178
+ """Handle a CHAIN_ANNOUNCE — trigger sync if we're behind."""
179
+ announced_height = data.get("block_index", 0)
180
+ our_height = self.store.get_height()
181
+ if announced_height > our_height + 1:
182
+ sender_id = data.get("producer_id", "")
183
+ if sender_id:
184
+ await self.sync_from_sentinel(sender_id, from_height=our_height + 1)
185
+
186
+ async def handle_tx_submit(
187
+ self, data: dict[str, Any]
188
+ ) -> TxSubmitResponse:
189
+ """Handle a TX_SUBMIT message from another colony."""
190
+ tx_json = data.get("tx_json", "{}")
191
+ try:
192
+ tx_dict = json.loads(tx_json)
193
+ tx = Transaction.from_dict(tx_dict)
194
+ except Exception as exc:
195
+ return TxSubmitResponse(tx_id="", accepted=False, reason=str(exc))
196
+
197
+ result = await self.submit_transaction(tx)
198
+ return TxSubmitResponse(
199
+ tx_id=result.tx_id,
200
+ accepted=result.accepted,
201
+ reason=result.reason,
202
+ )
203
+
204
+ async def get_chain_state_summary(
205
+ self, responder_id: str | None = None
206
+ ) -> ChainStateResponse:
207
+ """Build a ChainStateResponse summary."""
208
+ state = await self.get_state()
209
+ return ChainStateResponse(
210
+ height=state.height,
211
+ last_block_hash=state.last_block_hash,
212
+ n_colonies=len(state.colony_registry),
213
+ n_sentinels=len(state.active_sentinels()),
214
+ n_suspended=len(state.suspended_colonies),
215
+ network_id=state.network_id,
216
+ responder_id=responder_id or self.colony_id,
217
+ )
218
+
219
+ # ── Mempool helpers ──────────────────────────────────────────────────────
220
+
221
+ def drain_mempool(self, max_txs: int = 500) -> list[Transaction]:
222
+ """Drain pending transactions for block production."""
223
+ return self.store.get_mempool(limit=max_txs)
224
+
225
+ def evict_included(self, tx_ids: list[str]) -> None:
226
+ self.store.remove_from_mempool(tx_ids)
227
+
228
+ # ── Internal helpers ─────────────────────────────────────────────────────
229
+
230
+ async def _broadcast(self, msg: dict[str, Any]) -> None:
231
+ """Broadcast a message to all known peers (best-effort)."""
232
+ # In a real deployment this would iterate discovered peers.
233
+ # Here we call the send_message hook if provided.
234
+ if self._send_message:
235
+ state = await self.get_state()
236
+ for sentinel in state.active_sentinels():
237
+ try:
238
+ await self._send_message(sentinel.sentinel_id, msg)
239
+ except Exception as exc:
240
+ logger.debug(
241
+ "Failed to send to sentinel %s: %s", sentinel.sentinel_id, exc
242
+ )
243
+
244
+ def get_height(self) -> int:
245
+ return self.store.get_height()
246
+
247
+ @classmethod
248
+ def get_instance(cls) -> "ChainManager":
249
+ """Return the process-wide singleton backed by ~/.colony/data/chain.db."""
250
+ global _chain_manager_instance
251
+ if _chain_manager_instance is None:
252
+ colony_home = get_state_dir()
253
+ db_path = colony_home / "chain.db"
254
+ # Derive colony_id from genesis block cryptographic identity
255
+ colony_id = "local"
256
+ if db_path.exists():
257
+ try:
258
+ conn = sqlite3.connect(str(db_path))
259
+ row = conn.execute(
260
+ "SELECT raw_json FROM chain_blocks WHERE block_index = 0"
261
+ ).fetchone()
262
+ if row:
263
+ genesis_data = json.loads(row[0])
264
+ # Use the cryptographic colony_id (sha256 of Ed25519 pubkey)
265
+ # from genesis block metadata — not producer_id which is
266
+ # just a human-readable label and trivially spoofable.
267
+ colony_id = (
268
+ genesis_data.get("metadata", {}).get("genesis_colony_id")
269
+ or genesis_data.get("producer_id", "local")
270
+ )
271
+ conn.close()
272
+ except Exception:
273
+ pass
274
+ _chain_manager_instance = cls(db_path=db_path, colony_id=colony_id)
275
+ return _chain_manager_instance
276
+
277
+ def get_status(self) -> dict:
278
+ """Return chain status as a plain dict for the API router."""
279
+ height = self.store.get_height()
280
+ latest = self.store.get_latest_block()
281
+ mempool = self.store.get_mempool(limit=10000)
282
+ return {
283
+ "height": height,
284
+ "last_block_hash": latest.block_hash if latest else "0" * 64,
285
+ "last_block_timestamp": latest.timestamp if latest else None,
286
+ "colony_id": self.colony_id,
287
+ "mempool_size": len(mempool),
288
+ "protocol_version": "1.0.0",
289
+ "sync_status": "synced",
290
+ }
apsimo/chain/node.py ADDED
@@ -0,0 +1,163 @@
1
+ """Colony Node Identity — per-device identity within a Colony.
2
+
3
+ Each physical device running a Colony instance is a Node. Nodes have:
4
+ node_id — unique UUID per device, generated on first `colony start`
5
+ node keypair — Ed25519, independent from Colony keypair
6
+ node cert — signed by Colony private key, proving membership
7
+
8
+ The Colony is the logical identity. Nodes are the physical instances.
9
+ One Colony can run on many devices — each gets its own node_id.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import logging
16
+ import uuid
17
+ from datetime import datetime, timezone
18
+ from pathlib import Path
19
+ from typing import Optional
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ NODE_ID_FILE = "node-id"
24
+ NODE_KEYS_DIR = "node-keys"
25
+ NODE_CERT_FILE = "node-cert.json"
26
+
27
+
28
+ def get_or_create_node_id(state_dir: str | Path) -> str:
29
+ """Get or create the node_id for this device."""
30
+ id_path = Path(state_dir) / NODE_ID_FILE
31
+ if id_path.exists():
32
+ node_id = id_path.read_text().strip()
33
+ if node_id:
34
+ return node_id
35
+
36
+ node_id = str(uuid.uuid4())
37
+ id_path.parent.mkdir(parents=True, exist_ok=True)
38
+ id_path.write_text(node_id)
39
+ logger.info("Generated new node_id: %s", node_id)
40
+ return node_id
41
+
42
+
43
+ def ensure_node_keypair(state_dir: str | Path) -> "LocalKeyManager":
44
+ """Ensure this device has a node keypair. Generate if missing.
45
+
46
+ Returns a LocalKeyManager for the node keys.
47
+ """
48
+ from apsimo.chain.local_keys import LocalKeyManager
49
+
50
+ node_id = get_or_create_node_id(state_dir)
51
+ keys_dir = Path(state_dir) / NODE_KEYS_DIR
52
+ keys_dir.mkdir(parents=True, exist_ok=True)
53
+
54
+ if (keys_dir / "private.pem").exists():
55
+ return LocalKeyManager(keys_dir=keys_dir, colony_id=node_id)
56
+
57
+ km = LocalKeyManager.generate(keys_dir=keys_dir, colony_id=node_id)
58
+ logger.info("Generated node keypair for node %s", node_id)
59
+ return km
60
+
61
+
62
+ def create_node_certificate(
63
+ state_dir: str | Path,
64
+ colony_key_manager: Optional["LocalKeyManager"] = None,
65
+ ) -> dict:
66
+ """Create a node certificate signed by the Colony's private key.
67
+
68
+ The certificate binds: colony_id, node_id, node_public_key, issued_at.
69
+ It proves this device belongs to the Colony.
70
+ """
71
+ from apsimo.chain.local_keys import LocalKeyManager
72
+ from apsimo.chain.identity import get_or_create_colony_id
73
+
74
+ state_dir = Path(state_dir)
75
+ colony_id = get_or_create_colony_id(state_dir)
76
+ node_id = get_or_create_node_id(state_dir)
77
+ node_km = ensure_node_keypair(state_dir)
78
+ node_pubkey = node_km.public_key_hex()
79
+
80
+ cert = {
81
+ "colony_id": colony_id,
82
+ "node_id": node_id,
83
+ "node_public_key_ed25519": node_pubkey,
84
+ "issued_at": datetime.now(timezone.utc).isoformat(),
85
+ }
86
+
87
+ # Sign with Colony private key
88
+ if colony_key_manager is None:
89
+ colony_keys_dir = state_dir / "colony-keys"
90
+ passphrase = None # Caller should handle passphrase
91
+ if (colony_keys_dir / "private.pem").exists():
92
+ colony_key_manager = LocalKeyManager(keys_dir=colony_keys_dir, colony_id=colony_id, passphrase=passphrase)
93
+
94
+ if colony_key_manager is not None:
95
+ # Deterministic payload for signing (exclude signature field)
96
+ payload = json.dumps(cert, sort_keys=True, separators=(",", ":")).encode("utf-8")
97
+ cert["signature"] = colony_key_manager.sign(payload)
98
+ logger.info("Node certificate signed by Colony key")
99
+ else:
100
+ logger.warning("No Colony key manager available — node certificate is unsigned")
101
+
102
+ cert_path = state_dir / NODE_CERT_FILE
103
+ cert_path.write_text(json.dumps(cert, indent=2) + "\n")
104
+ return cert
105
+
106
+
107
+ def verify_node_certificate(cert: dict, colony_public_key_hex: str) -> bool:
108
+ """Verify a node certificate's signature against a Colony public key."""
109
+ from apsimo.chain.identity import _verify_ed25519_signature
110
+
111
+ signature = cert.get("signature")
112
+ if not signature:
113
+ return False
114
+
115
+ payload = {k: v for k, v in cert.items() if k != "signature"}
116
+ payload_bytes = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
117
+ return _verify_ed25519_signature(colony_public_key_hex, payload_bytes, signature)
118
+
119
+
120
+ def load_node_certificate(state_dir: str | Path) -> Optional[dict]:
121
+ """Load the node certificate from disk."""
122
+ cert_path = Path(state_dir) / NODE_CERT_FILE
123
+ if not cert_path.exists():
124
+ return None
125
+ try:
126
+ return json.loads(cert_path.read_text())
127
+ except (json.JSONDecodeError, OSError):
128
+ return None
129
+
130
+
131
+ def get_node_info(state_dir: str | Path) -> dict:
132
+ """Get full node identity info: node_id, public key, cert status."""
133
+ state_dir = Path(state_dir)
134
+
135
+ node_id = None
136
+ node_pubkey = None
137
+ certified = False
138
+ issued_at = None
139
+
140
+ node_id_path = state_dir / NODE_ID_FILE
141
+ if node_id_path.exists():
142
+ node_id = node_id_path.read_text().strip() or None
143
+
144
+ node_keys_dir = state_dir / NODE_KEYS_DIR
145
+ if (node_keys_dir / "private.pem").exists():
146
+ try:
147
+ from apsimo.chain.local_keys import LocalKeyManager
148
+ km = LocalKeyManager(keys_dir=node_keys_dir, colony_id=node_id or "unknown")
149
+ node_pubkey = km.public_key_hex()
150
+ except Exception:
151
+ pass
152
+
153
+ cert = load_node_certificate(state_dir)
154
+ if cert and cert.get("signature"):
155
+ certified = True
156
+ issued_at = cert.get("issued_at")
157
+
158
+ return {
159
+ "node_id": node_id,
160
+ "node_public_key": node_pubkey,
161
+ "certified": certified,
162
+ "issued_at": issued_at,
163
+ }