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,42 @@
1
+ """Shared phone gateway resolution.
2
+
3
+ Replaces the three hardcoded _PHONE_GATEWAYS tuples scattered across
4
+ contacts/store.py, identity/resolver.py, and contacts/world_bridge.py.
5
+
6
+ Reads from the channel store when available; falls back to a built-in
7
+ set so the system works before any channels are registered.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ from typing import Optional
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ _BUILTIN_PHONE_GATEWAYS = frozenset({
18
+ "imessage", "sms", "signal", "whatsapp",
19
+ })
20
+
21
+ _channel_store_ref: Optional[object] = None
22
+
23
+
24
+ def set_channel_store_ref(store) -> None:
25
+ global _channel_store_ref
26
+ _channel_store_ref = store
27
+
28
+
29
+ def get_phone_gateways() -> frozenset[str]:
30
+ """Return the set of gateways that use phone-number identity.
31
+
32
+ When a channel store is available, returns the union of registered
33
+ channels with phone_identity_unification=True and the built-in set.
34
+ Without a channel store, returns only the built-in set.
35
+ """
36
+ if _channel_store_ref is not None:
37
+ try:
38
+ registered = _channel_store_ref.get_phone_gateways()
39
+ return frozenset(registered | _BUILTIN_PHONE_GATEWAYS)
40
+ except Exception:
41
+ logger.debug("Channel store query failed, using built-in set", exc_info=True)
42
+ return _BUILTIN_PHONE_GATEWAYS
@@ -0,0 +1,188 @@
1
+ """ConversationPresenceStore — who has been seen in which conversation (L1.1).
2
+
3
+ The conversation participant registry the leveled cross-contact tom2 system
4
+ (docs-level: TOM2 levels) is built on. Every attributed turn records
5
+ ``(conversation_key, contact_id, resolution method, group_id)``; downstream
6
+ consumers (the environment-risk classifier, subject-presence exclusion) read a
7
+ windowed census of a conversation.
8
+
9
+ Properties:
10
+
11
+ * **Passive.** Recording is fed from the turns/sync attribution chokepoint
12
+ (after the ParticipantResolver has decided WHO the turn came from) and
13
+ changes nothing about turn processing. ``COLONY_CONV_PRESENCE`` (default
14
+ on) can disable recording entirely.
15
+ * **Identity only.** Rows carry contact ids, resolution methods and opaque
16
+ conversation keys — never message content.
17
+ * **System excluded.** The reserved machine sentinel (``system``) is never
18
+ recorded: machine turns must not shape a conversation's human census.
19
+ * **Reads fail closed.** Read methods PROPAGATE storage errors instead of
20
+ returning an empty (i.e. "nobody here") census — an empty answer from a
21
+ broken store would read as a SAFE signal to the risk classifier. Callers
22
+ must catch and treat any error as "unknown, assume hostile".
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import logging
28
+ import os
29
+ import sqlite3
30
+ import threading
31
+ from datetime import datetime, timedelta, timezone
32
+ from typing import Any, Dict, List, Optional
33
+
34
+ from apsimo.identity.participants import SYSTEM_CONTACT_ID
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+ #: Resolution methods considered STRONG identity evidence: the contact was
39
+ #: matched by a verified handle or arrived pre-resolved as a canonical contact
40
+ #: id. Scoped-name matches (unverified link proposals), shadow contacts and
41
+ #: client-claimed ids are NOT strong.
42
+ STRONG_METHODS = frozenset({"handle", "contact_id"})
43
+
44
+
45
+ def conv_presence_enabled() -> bool:
46
+ """COLONY_CONV_PRESENCE (default on): passive presence recording."""
47
+ return os.environ.get("COLONY_CONV_PRESENCE", "on").strip().lower() not in (
48
+ "off", "0", "false", "no")
49
+
50
+
51
+ def _now() -> datetime:
52
+ return datetime.now(tz=timezone.utc)
53
+
54
+
55
+ class ConversationPresenceStore:
56
+ """SQLite-backed registry of who was seen in which conversation."""
57
+
58
+ def __init__(self, db_path: Optional[str] = None) -> None:
59
+ self._lock = threading.RLock()
60
+ self._conn = sqlite3.connect(db_path or ":memory:",
61
+ check_same_thread=False)
62
+ self._conn.row_factory = sqlite3.Row
63
+ with self._lock:
64
+ self._conn.execute("""
65
+ CREATE TABLE IF NOT EXISTS conversation_presence (
66
+ conversation_key TEXT NOT NULL,
67
+ contact_id TEXT NOT NULL,
68
+ method TEXT NOT NULL DEFAULT '',
69
+ group_id TEXT NOT NULL DEFAULT '',
70
+ first_seen_at TEXT NOT NULL,
71
+ last_seen_at TEXT NOT NULL,
72
+ turns INTEGER NOT NULL DEFAULT 0,
73
+ PRIMARY KEY (conversation_key, contact_id)
74
+ )""")
75
+ self._conn.execute(
76
+ "CREATE INDEX IF NOT EXISTS idx_conv_presence_contact "
77
+ "ON conversation_presence(contact_id)")
78
+ self._conn.commit()
79
+
80
+ # -- writes -----------------------------------------------------------
81
+ def record(self, conversation_key: str, contact_id: str, *,
82
+ method: str = "", group_id: str = "") -> bool:
83
+ """Record one sighting of ``contact_id`` in ``conversation_key``.
84
+
85
+ Returns True when a row was written/refreshed. The system sentinel
86
+ and empty ids are silently skipped (never an error — the chokepoint
87
+ must not care); the COLONY_CONV_PRESENCE gate turns the whole write
88
+ into a no-op. ``method`` stores the LATEST resolution method for the
89
+ pair: a weak latest sighting correctly downgrades the row, and a row
90
+ can only read as strong when the most recent turn actually resolved
91
+ strongly (by verified handle / canonical contact id).
92
+ """
93
+ if not conv_presence_enabled():
94
+ return False
95
+ conversation_key = str(conversation_key or "").strip()
96
+ contact_id = str(contact_id or "").strip()
97
+ if not conversation_key or not contact_id:
98
+ return False
99
+ if contact_id == SYSTEM_CONTACT_ID:
100
+ return False
101
+ now = _now().isoformat()
102
+ method = str(method or "").strip().lower()
103
+ group_id = str(group_id or "").strip()
104
+ with self._lock:
105
+ self._conn.execute(
106
+ """INSERT INTO conversation_presence
107
+ (conversation_key, contact_id, method, group_id,
108
+ first_seen_at, last_seen_at, turns)
109
+ VALUES (?,?,?,?,?,?,1)
110
+ ON CONFLICT(conversation_key, contact_id) DO UPDATE SET
111
+ method=excluded.method,
112
+ group_id=CASE WHEN excluded.group_id != ''
113
+ THEN excluded.group_id ELSE group_id END,
114
+ last_seen_at=excluded.last_seen_at,
115
+ turns=turns+1""",
116
+ (conversation_key, contact_id, method, group_id, now, now))
117
+ self._conn.commit()
118
+ return True
119
+
120
+ # -- reads (fail closed: errors PROPAGATE) ------------------------------
121
+ def census(self, conversation_key: str,
122
+ window_hours: float = 48.0) -> List[Dict[str, Any]]:
123
+ """Participants seen in this conversation within the window.
124
+
125
+ Raises on any storage error — callers MUST treat an exception as
126
+ "census unknown" (hostile), never as "empty room".
127
+ """
128
+ cutoff = (_now() - timedelta(hours=float(window_hours))).isoformat()
129
+ with self._lock:
130
+ rows = self._conn.execute(
131
+ "SELECT * FROM conversation_presence "
132
+ "WHERE conversation_key=? AND last_seen_at >= ? "
133
+ "ORDER BY last_seen_at DESC",
134
+ (str(conversation_key or ""), cutoff)).fetchall()
135
+ return [dict(r) for r in rows]
136
+
137
+ def is_present(self, conversation_key: str, contact_id: str,
138
+ window_hours: float = 48.0) -> bool:
139
+ """True when the contact was seen in the conversation within the
140
+ window. Raises on storage errors (see census)."""
141
+ cutoff = (_now() - timedelta(hours=float(window_hours))).isoformat()
142
+ with self._lock:
143
+ row = self._conn.execute(
144
+ "SELECT 1 FROM conversation_presence "
145
+ "WHERE conversation_key=? AND contact_id=? AND last_seen_at >= ?",
146
+ (str(conversation_key or ""), str(contact_id or ""),
147
+ cutoff)).fetchone()
148
+ return row is not None
149
+
150
+ def cooccurred(self, contact_a: str, contact_b: str,
151
+ within_days: float = 30.0) -> bool:
152
+ """True when both contacts were seen in at least ONE common
153
+ conversation within the window (mutual-knowledge evidence: they
154
+ demonstrably know of each other). Raises on storage errors."""
155
+ a = str(contact_a or "").strip()
156
+ b = str(contact_b or "").strip()
157
+ if not a or not b or a == b:
158
+ return False
159
+ cutoff = (_now() - timedelta(days=float(within_days))).isoformat()
160
+ with self._lock:
161
+ row = self._conn.execute(
162
+ """SELECT 1 FROM conversation_presence pa
163
+ JOIN conversation_presence pb
164
+ ON pa.conversation_key = pb.conversation_key
165
+ WHERE pa.contact_id=? AND pb.contact_id=?
166
+ AND pa.last_seen_at >= ? AND pb.last_seen_at >= ?
167
+ LIMIT 1""", (a, b, cutoff, cutoff)).fetchone()
168
+ return row is not None
169
+
170
+ def counts(self) -> Dict[str, Any]:
171
+ """Aggregate observability (numbers only)."""
172
+ with self._lock:
173
+ convs = self._conn.execute(
174
+ "SELECT COUNT(DISTINCT conversation_key) n "
175
+ "FROM conversation_presence").fetchone()
176
+ contacts = self._conn.execute(
177
+ "SELECT COUNT(DISTINCT contact_id) n "
178
+ "FROM conversation_presence").fetchone()
179
+ rows = self._conn.execute(
180
+ "SELECT COUNT(*) n FROM conversation_presence").fetchone()
181
+ return {"rows": int(rows["n"]), "conversations": int(convs["n"]),
182
+ "contacts": int(contacts["n"])}
183
+
184
+ def close(self) -> None:
185
+ try:
186
+ self._conn.close()
187
+ except Exception:
188
+ pass
@@ -0,0 +1,235 @@
1
+ """Channel registration API -- /v1/channels/ endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ipaddress
6
+ import logging
7
+ import os
8
+ from typing import Optional
9
+ from urllib.parse import urlparse
10
+
11
+ from fastapi import APIRouter, Header, HTTPException, status
12
+ from pydantic import BaseModel
13
+
14
+ from apsimo.channels.manifest import ChannelManifest
15
+ from apsimo.channels.store import ChannelStore
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ router = APIRouter(prefix="/v1/channels", tags=["channels"])
20
+
21
+ _channel_store: Optional[ChannelStore] = None
22
+
23
+
24
+ def set_channel_store(store: ChannelStore) -> None:
25
+ global _channel_store
26
+ _channel_store = store
27
+
28
+
29
+ def get_channel_store() -> Optional[ChannelStore]:
30
+ return _channel_store
31
+
32
+
33
+ def _require_store() -> ChannelStore:
34
+ if _channel_store is None:
35
+ raise HTTPException(
36
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
37
+ detail="Channel store not initialized",
38
+ )
39
+ return _channel_store
40
+
41
+
42
+ # ── Request / Response models ────────────────────────────────────────────
43
+
44
+
45
+ class RegisterResponse(BaseModel):
46
+ channel_key: str
47
+ registered_at: str
48
+ channel_token: str
49
+
50
+
51
+ class ChannelInfo(BaseModel):
52
+ channel_key: str
53
+ display_name: str
54
+ gateway_family: str
55
+ status: str
56
+ registered_at: str
57
+ last_seen_at: Optional[str] = None
58
+ supports_media: bool = False
59
+ supports_reactions: bool = False
60
+ supports_voice: bool = False
61
+ supports_rich_text: bool = False
62
+ max_message_length: Optional[int] = None
63
+ phone_identity_unification: bool = False
64
+ provides_channel_id: bool = False
65
+ delivery_protocol: str = "hermes"
66
+ delivery_aliases: list[str] = []
67
+ home_chat_id: Optional[str] = None
68
+
69
+
70
+ # ── Webhook validation ───────────────────────────────────────────────────
71
+
72
+
73
+ _PRIVATE_NETWORKS = [
74
+ ipaddress.ip_network("10.0.0.0/8"),
75
+ ipaddress.ip_network("172.16.0.0/12"),
76
+ ipaddress.ip_network("192.168.0.0/16"),
77
+ ipaddress.ip_network("169.254.0.0/16"),
78
+ ipaddress.ip_network("127.0.0.0/8"),
79
+ ipaddress.ip_network("::1/128"),
80
+ ipaddress.ip_network("fd00::/8"),
81
+ ipaddress.ip_network("fe80::/10"),
82
+ ]
83
+
84
+
85
+ def _validate_webhook(url: str) -> None:
86
+ parsed = urlparse(url)
87
+ if parsed.scheme not in ("http", "https"):
88
+ raise HTTPException(
89
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
90
+ detail=f"Webhook URL must be http or https, got '{parsed.scheme}'",
91
+ )
92
+ if not parsed.hostname:
93
+ raise HTTPException(
94
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
95
+ detail="Webhook URL has no hostname",
96
+ )
97
+
98
+ allow_private = os.environ.get(
99
+ "COLONY_ALLOW_PRIVATE_WEBHOOKS", ""
100
+ ).lower() in ("true", "1", "yes")
101
+
102
+ if not allow_private:
103
+ try:
104
+ addr = ipaddress.ip_address(parsed.hostname)
105
+ if any(addr in net for net in _PRIVATE_NETWORKS):
106
+ raise HTTPException(
107
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
108
+ detail=(
109
+ f"Webhook URL resolves to private address {addr}. "
110
+ "Set COLONY_ALLOW_PRIVATE_WEBHOOKS=true to allow."
111
+ ),
112
+ )
113
+ except ValueError:
114
+ pass
115
+
116
+
117
+ # ── Endpoints ────────────────────────────────────────────────────────────
118
+
119
+
120
+ @router.post("/register", status_code=status.HTTP_201_CREATED)
121
+ async def register_channel(
122
+ manifest: ChannelManifest,
123
+ x_channel_token: Optional[str] = Header(None),
124
+ ) -> RegisterResponse:
125
+ store = _require_store()
126
+
127
+ if manifest.delivery_webhook:
128
+ _validate_webhook(manifest.delivery_webhook)
129
+
130
+ try:
131
+ registered = store.register(manifest, channel_token=x_channel_token)
132
+ except ValueError as exc:
133
+ raise HTTPException(
134
+ status_code=status.HTTP_409_CONFLICT, detail=str(exc)
135
+ )
136
+
137
+ return RegisterResponse(
138
+ channel_key=registered.channel_key,
139
+ registered_at=registered.registered_at,
140
+ channel_token=registered.channel_token,
141
+ )
142
+
143
+
144
+ @router.get("", response_model=list[ChannelInfo])
145
+ async def list_channels() -> list[ChannelInfo]:
146
+ store = _require_store()
147
+ return [_to_info(ch) for ch in store.list_all()]
148
+
149
+
150
+ @router.get("/{channel_key}", response_model=ChannelInfo)
151
+ async def get_channel(channel_key: str) -> ChannelInfo:
152
+ store = _require_store()
153
+ ch = store.get(channel_key)
154
+ if ch is None:
155
+ raise HTTPException(
156
+ status_code=status.HTTP_404_NOT_FOUND,
157
+ detail=f"Channel '{channel_key}' not found",
158
+ )
159
+ return _to_info(ch)
160
+
161
+
162
+ @router.put("/{channel_key}", response_model=ChannelInfo)
163
+ async def update_channel(
164
+ channel_key: str,
165
+ manifest: ChannelManifest,
166
+ x_channel_token: str = Header(...),
167
+ ) -> ChannelInfo:
168
+ store = _require_store()
169
+
170
+ if manifest.channel_key != channel_key:
171
+ raise HTTPException(
172
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
173
+ detail="channel_key in body must match URL path",
174
+ )
175
+ if manifest.delivery_webhook:
176
+ _validate_webhook(manifest.delivery_webhook)
177
+
178
+ if not store.verify_token(channel_key, x_channel_token):
179
+ raise HTTPException(
180
+ status_code=status.HTTP_403_FORBIDDEN,
181
+ detail="Invalid channel token",
182
+ )
183
+
184
+ try:
185
+ registered = store.register(manifest, channel_token=x_channel_token)
186
+ except ValueError as exc:
187
+ raise HTTPException(
188
+ status_code=status.HTTP_409_CONFLICT, detail=str(exc)
189
+ )
190
+
191
+ return _to_info(registered)
192
+
193
+
194
+ @router.delete("/{channel_key}", status_code=status.HTTP_204_NO_CONTENT)
195
+ async def delete_channel(
196
+ channel_key: str,
197
+ x_channel_token: str = Header(...),
198
+ ) -> None:
199
+ store = _require_store()
200
+
201
+ if not store.verify_token(channel_key, x_channel_token):
202
+ raise HTTPException(
203
+ status_code=status.HTTP_403_FORBIDDEN,
204
+ detail="Invalid channel token",
205
+ )
206
+ if not store.revoke(channel_key):
207
+ raise HTTPException(
208
+ status_code=status.HTTP_404_NOT_FOUND,
209
+ detail=f"Channel '{channel_key}' not found",
210
+ )
211
+
212
+
213
+ # ── Helpers ──────────────────────────────────────────────────────────────
214
+
215
+
216
+ def _to_info(ch) -> ChannelInfo:
217
+ m = ch.manifest
218
+ return ChannelInfo(
219
+ channel_key=ch.channel_key,
220
+ display_name=ch.display_name,
221
+ gateway_family=ch.gateway_family,
222
+ status=ch.status,
223
+ registered_at=ch.registered_at,
224
+ last_seen_at=ch.last_seen_at,
225
+ supports_media=m.supports_media,
226
+ supports_reactions=m.supports_reactions,
227
+ supports_voice=m.supports_voice,
228
+ supports_rich_text=m.supports_rich_text,
229
+ max_message_length=m.max_message_length,
230
+ phone_identity_unification=m.phone_identity_unification,
231
+ provides_channel_id=m.provides_channel_id,
232
+ delivery_protocol=m.delivery_protocol,
233
+ delivery_aliases=m.delivery_aliases,
234
+ home_chat_id=m.home_chat_id,
235
+ )
@@ -0,0 +1,231 @@
1
+ """Channel registration store -- backed by colony-channels.db.
2
+
3
+ Tracks which channels are registered with this Colony instance, their
4
+ capabilities, and authentication tokens.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import hmac
11
+ import json
12
+ import logging
13
+ import secrets
14
+ import sqlite3
15
+ from dataclasses import dataclass
16
+ from datetime import datetime, timezone
17
+ from pathlib import Path
18
+ from typing import Optional
19
+
20
+ from apsimo.channels.manifest import ChannelManifest
21
+ from apsimo.migrations import run_migrations_sync
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ _MIGRATIONS_DIR = Path(__file__).parent / "migrations"
26
+
27
+
28
+ @dataclass
29
+ class RegisteredChannel:
30
+ channel_key: str
31
+ display_name: str
32
+ gateway_family: str
33
+ manifest: ChannelManifest
34
+ channel_token: str
35
+ registered_at: str
36
+ last_seen_at: Optional[str]
37
+ status: str
38
+
39
+
40
+ class ChannelStore:
41
+ """SQLite-backed channel registration store."""
42
+
43
+ def __init__(self, db_path: str = ":memory:") -> None:
44
+ self._db_path = db_path
45
+ self._conn: Optional[sqlite3.Connection] = None
46
+
47
+ def connect(self) -> None:
48
+ if self._db_path != ":memory:":
49
+ Path(self._db_path).parent.mkdir(parents=True, exist_ok=True)
50
+ self._conn = sqlite3.connect(self._db_path, check_same_thread=False)
51
+ self._conn.row_factory = sqlite3.Row
52
+ self._conn.execute("PRAGMA journal_mode=WAL")
53
+ self._conn.execute("PRAGMA foreign_keys=ON")
54
+ run_migrations_sync(self._conn, _MIGRATIONS_DIR)
55
+
56
+ def close(self) -> None:
57
+ if self._conn:
58
+ self._conn.close()
59
+ self._conn = None
60
+
61
+ def _require_conn(self) -> sqlite3.Connection:
62
+ if not self._conn:
63
+ raise RuntimeError("ChannelStore not connected")
64
+ return self._conn
65
+
66
+ # ── Registration ─────────────────────────────────────────────────────
67
+
68
+ def register(
69
+ self,
70
+ manifest: ChannelManifest,
71
+ *,
72
+ channel_token: Optional[str] = None,
73
+ ) -> RegisteredChannel:
74
+ """Register a new channel or update an existing one.
75
+
76
+ If the channel_key already exists and the provided token matches,
77
+ the registration is updated (upsert). If no token is provided for
78
+ a new registration, one is generated.
79
+
80
+ Raises ValueError on token mismatch (409 at the API layer).
81
+ """
82
+ conn = self._require_conn()
83
+ now = datetime.now(timezone.utc).isoformat()
84
+
85
+ existing = self.get(manifest.channel_key)
86
+
87
+ if existing is not None:
88
+ if not channel_token or not self.verify_token(
89
+ manifest.channel_key, channel_token
90
+ ):
91
+ raise ValueError(
92
+ f"Channel '{manifest.channel_key}' already registered; "
93
+ "provide the correct channel_token to update"
94
+ )
95
+ conn.execute(
96
+ """UPDATE channels
97
+ SET display_name = ?, gateway_family = ?,
98
+ manifest_json = ?, last_seen_at = ?
99
+ WHERE channel_key = ?""",
100
+ (
101
+ manifest.display_name,
102
+ manifest.gateway_family,
103
+ manifest.model_dump_json(),
104
+ now,
105
+ manifest.channel_key,
106
+ ),
107
+ )
108
+ conn.commit()
109
+ return self.get(manifest.channel_key) # type: ignore[return-value]
110
+
111
+ token = channel_token or _generate_token()
112
+ token_hash = _hash_token(token)
113
+
114
+ conn.execute(
115
+ """INSERT INTO channels
116
+ (channel_key, display_name, gateway_family,
117
+ manifest_json, channel_token, registered_at, last_seen_at, status)
118
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'active')""",
119
+ (
120
+ manifest.channel_key,
121
+ manifest.display_name,
122
+ manifest.gateway_family,
123
+ manifest.model_dump_json(),
124
+ token_hash,
125
+ now,
126
+ now,
127
+ ),
128
+ )
129
+ conn.commit()
130
+
131
+ registered = self.get(manifest.channel_key)
132
+ assert registered is not None
133
+ registered.channel_token = token
134
+ return registered
135
+
136
+ # ── Queries ──────────────────────────────────────────────────────────
137
+
138
+ def get(self, channel_key: str) -> Optional[RegisteredChannel]:
139
+ conn = self._require_conn()
140
+ cur = conn.execute(
141
+ "SELECT * FROM channels WHERE channel_key = ?", (channel_key,)
142
+ )
143
+ row = cur.fetchone()
144
+ return _row_to_channel(row) if row else None
145
+
146
+ def list_active(self) -> list[RegisteredChannel]:
147
+ conn = self._require_conn()
148
+ cur = conn.execute(
149
+ "SELECT * FROM channels WHERE status = 'active' ORDER BY channel_key"
150
+ )
151
+ return [_row_to_channel(row) for row in cur.fetchall()]
152
+
153
+ def list_all(self) -> list[RegisteredChannel]:
154
+ conn = self._require_conn()
155
+ cur = conn.execute("SELECT * FROM channels ORDER BY channel_key")
156
+ return [_row_to_channel(row) for row in cur.fetchall()]
157
+
158
+ def get_phone_gateways(self) -> set[str]:
159
+ """Return channel_keys with phone_identity_unification enabled."""
160
+ return {
161
+ ch.channel_key
162
+ for ch in self.list_active()
163
+ if ch.manifest.phone_identity_unification
164
+ }
165
+
166
+ # ── Mutations ────────────────────────────────────────────────────────
167
+
168
+ def touch(self, channel_key: str) -> None:
169
+ """Update last_seen_at for the given channel."""
170
+ conn = self._require_conn()
171
+ now = datetime.now(timezone.utc).isoformat()
172
+ conn.execute(
173
+ "UPDATE channels SET last_seen_at = ? WHERE channel_key = ?",
174
+ (now, channel_key),
175
+ )
176
+ conn.commit()
177
+
178
+ def revoke(self, channel_key: str) -> bool:
179
+ conn = self._require_conn()
180
+ cur = conn.execute(
181
+ "UPDATE channels SET status = 'revoked' WHERE channel_key = ?",
182
+ (channel_key,),
183
+ )
184
+ conn.commit()
185
+ return cur.rowcount > 0
186
+
187
+ def delete(self, channel_key: str) -> bool:
188
+ conn = self._require_conn()
189
+ cur = conn.execute(
190
+ "DELETE FROM channels WHERE channel_key = ?", (channel_key,)
191
+ )
192
+ conn.commit()
193
+ return cur.rowcount > 0
194
+
195
+ # ── Auth ─────────────────────────────────────────────────────────────
196
+
197
+ def verify_token(self, channel_key: str, token: str) -> bool:
198
+ conn = self._require_conn()
199
+ cur = conn.execute(
200
+ "SELECT channel_token FROM channels WHERE channel_key = ?",
201
+ (channel_key,),
202
+ )
203
+ row = cur.fetchone()
204
+ if not row:
205
+ return False
206
+ return hmac.compare_digest(row["channel_token"], _hash_token(token))
207
+
208
+
209
+ # ── Helpers ──────────────────────────────────────────────────────────────
210
+
211
+
212
+ def _generate_token() -> str:
213
+ return "ch_" + secrets.token_urlsafe(32)
214
+
215
+
216
+ def _hash_token(token: str) -> str:
217
+ return hashlib.sha256(token.encode()).hexdigest()
218
+
219
+
220
+ def _row_to_channel(row: sqlite3.Row) -> RegisteredChannel:
221
+ manifest = ChannelManifest.model_validate_json(row["manifest_json"])
222
+ return RegisteredChannel(
223
+ channel_key=row["channel_key"],
224
+ display_name=row["display_name"],
225
+ gateway_family=row["gateway_family"],
226
+ manifest=manifest,
227
+ channel_token=row["channel_token"],
228
+ registered_at=row["registered_at"],
229
+ last_seen_at=row["last_seen_at"],
230
+ status=row["status"],
231
+ )