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,526 @@
1
+ """ChannelRegistry — resolves per-person delivery channels from multiple sources.
2
+
3
+ Resolution priority (highest first):
4
+ 1. Environment variables (COLONY_CHANNEL_*)
5
+ 2. JSON config file ({COLONY_STATE_DIR}/data/channels.json)
6
+ 3. Contact handles (phone → chat platform DM inference, configurable mapping)
7
+ 4. Home channel fallback (WHATSAPP_HOME_CHANNEL, TELEGRAM_HOME_CHANNEL, etc.)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import inspect
13
+ import json
14
+ import logging
15
+ import os
16
+ import re
17
+ from dataclasses import dataclass
18
+ from pathlib import Path
19
+ from typing import Any, Dict, List, Optional
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ @dataclass
25
+ class Channel:
26
+ """A resolved delivery channel."""
27
+
28
+ platform: str # "whatsapp", "telegram", "discord", ...
29
+ chat_id: str # platform-specific chat identifier
30
+ channel_type: str # "dm" | "home" | "work" | "custom"
31
+
32
+
33
+ class ChannelRegistry:
34
+ """Resolves per-person delivery channels from multiple sources."""
35
+
36
+ # Default gateway-to-platform mapping (override via COLONY_CHANNEL_GATEWAY_MAP)
37
+ DEFAULT_GATEWAY_MAP = {
38
+ "imessage": "whatsapp",
39
+ "sms": "whatsapp",
40
+ "telegram": "telegram",
41
+ "signal": "signal",
42
+ # "email" excluded — not a chat platform
43
+ }
44
+
45
+ def __init__(
46
+ self,
47
+ env_channels: Dict[str, Dict[str, Channel]],
48
+ json_channels: Dict[str, Dict[str, Channel]],
49
+ fallback_channels: Dict[str, Channel],
50
+ handle_inference: bool,
51
+ gateway_map: Dict[str, str],
52
+ contacts_store: Optional[Any] = None,
53
+ owner_contact_id: Optional[str] = None,
54
+ channel_store: Optional[Any] = None,
55
+ ) -> None:
56
+ self._env = env_channels
57
+ self._json = json_channels
58
+ self._fallback = fallback_channels
59
+ self._handle_inference = handle_inference
60
+ self._gateway_map = gateway_map
61
+ self._contacts_store = contacts_store
62
+ self._channel_store = channel_store
63
+ self._json_path_value: str = ""
64
+ self._owner_contact_id = self._normalize_person_id(owner_contact_id) if owner_contact_id else None
65
+
66
+ # ------------------------------------------------------------------
67
+ # Public API
68
+ # ------------------------------------------------------------------
69
+
70
+ def resolve(
71
+ self,
72
+ person_id: str,
73
+ channel_type: str = "home",
74
+ ) -> Optional[Channel]:
75
+ """Resolve a delivery channel for ``person_id`` and ``channel_type``.
76
+
77
+ Resolution order:
78
+ 1. Env vars (COLONY_CHANNEL_DM_* / COLONY_CHANNEL_HOME_*)
79
+ 2. JSON config (contacts section)
80
+ 3. JSON fallback
81
+ 4. Contact handle inference (DM only)
82
+ 5. Home channel env vars (home only)
83
+
84
+ Returns ``None`` if no channel can be resolved.
85
+ """
86
+ normalized_id = self._normalize_person_id(person_id)
87
+
88
+ # 1. Env vars (highest priority)
89
+ env_person = self._env.get(normalized_id, {})
90
+ if channel_type in env_person:
91
+ return env_person[channel_type]
92
+
93
+ # 1b. Owner alias — if person_id matches owner contact ID, also check "owner"
94
+ if self._owner_contact_id and normalized_id == self._owner_contact_id:
95
+ env_owner = self._env.get("owner", {})
96
+ if channel_type in env_owner:
97
+ return env_owner[channel_type]
98
+
99
+ # 2. JSON config (contacts section)
100
+ json_person = self._json.get(normalized_id, {})
101
+ if channel_type in json_person:
102
+ return json_person[channel_type]
103
+
104
+ # 2b. Owner alias for JSON
105
+ if self._owner_contact_id and normalized_id == self._owner_contact_id:
106
+ json_owner = self._json.get("owner", {})
107
+ if channel_type in json_owner:
108
+ return json_owner[channel_type]
109
+
110
+ # 3. JSON fallback
111
+ if channel_type in self._fallback:
112
+ return self._fallback[channel_type]
113
+
114
+ # 4. Contact handle inference (DM only)
115
+ if channel_type == "dm" and self._handle_inference and self._contacts_store:
116
+ inferred = self._infer_from_handles(normalized_id)
117
+ if inferred:
118
+ return inferred
119
+
120
+ # 5. Registered channels (dynamic): a live registered channel that declares a
121
+ # home_chat_id can serve as the home route. Freshest last_seen_at wins, so
122
+ # routing follows the surface that is actually alive. Explicit env/JSON
123
+ # config above still outranks this (operator intent beats observation).
124
+ if channel_type == "home":
125
+ store_home = self._resolve_home_from_store()
126
+ if store_home:
127
+ return store_home
128
+
129
+ # 6. Home channel env vars (home only, lowest priority)
130
+ if channel_type == "home":
131
+ home = self._resolve_home_from_env()
132
+ if home:
133
+ return home
134
+
135
+ return None
136
+
137
+ async def resolve_exact_verified_dm(
138
+ self,
139
+ person_id: str,
140
+ *,
141
+ platform: str,
142
+ ) -> Optional[Channel]:
143
+ """Resolve one canonical contact's exact verified platform handle.
144
+
145
+ This is the asynchronous, fail-closed path for a delivery boundary
146
+ that must address a specific contact. Unlike legacy handle inference,
147
+ it does not map a phone-bearing handle from one gateway to another and
148
+ it never falls back to a home/shared channel. Authority is deliberately
149
+ out of scope: the caller's policy boundary must still decide whether a
150
+ message may be sent.
151
+ """
152
+ if self._contacts_store is None:
153
+ return None
154
+ canonical_id = str(person_id or "").strip()
155
+ exact_platform = str(platform or "").strip().lower()
156
+ if not canonical_id or not exact_platform:
157
+ return None
158
+
159
+ get_contact = getattr(self._contacts_store, "get", None)
160
+ get_handles = getattr(self._contacts_store, "get_handles", None)
161
+ if not callable(get_contact) or not callable(get_handles):
162
+ return None
163
+ try:
164
+ contact = get_contact(canonical_id)
165
+ if inspect.isawaitable(contact):
166
+ contact = await contact
167
+ if (
168
+ contact is None
169
+ or str(getattr(contact, "contact_id", "")) != canonical_id
170
+ or getattr(contact, "deleted_at", None)
171
+ ):
172
+ return None
173
+
174
+ handles = get_handles(canonical_id)
175
+ if inspect.isawaitable(handles):
176
+ handles = await handles
177
+ except Exception:
178
+ logger.debug(
179
+ "Exact verified DM resolution failed for %s",
180
+ canonical_id,
181
+ exc_info=True,
182
+ )
183
+ return None
184
+
185
+ candidates: set[str] = set()
186
+ for handle in handles or ():
187
+ handle_contact = getattr(handle, "contact_id", canonical_id)
188
+ if str(handle_contact) != canonical_id:
189
+ logger.warning(
190
+ "Exact DM resolution rejected a cross-contact handle record"
191
+ )
192
+ return None
193
+ gateway = str(getattr(handle, "gateway", "") or "").strip().lower()
194
+ if gateway != exact_platform or getattr(handle, "verified", None) is not True:
195
+ continue
196
+ address = getattr(handle, "address", None)
197
+ if not isinstance(address, str) or not address:
198
+ continue
199
+ candidates.add(address)
200
+
201
+ if len(candidates) != 1:
202
+ if candidates:
203
+ logger.warning(
204
+ "Exact verified DM resolution is ambiguous for contact %s on %s",
205
+ canonical_id,
206
+ exact_platform,
207
+ )
208
+ return None
209
+ return Channel(
210
+ platform=exact_platform,
211
+ chat_id=next(iter(candidates)),
212
+ channel_type="dm",
213
+ )
214
+
215
+ def _resolve_home_from_store(self) -> Optional[Channel]:
216
+ """Home route from the channel registration DB (Framework Part 4)."""
217
+ if self._channel_store is None:
218
+ return None
219
+ try:
220
+ candidates = [
221
+ ch for ch in self._channel_store.list_active()
222
+ if getattr(ch.manifest, "home_chat_id", None)
223
+ ]
224
+ if not candidates:
225
+ return None
226
+ candidates.sort(key=lambda c: c.last_seen_at or c.registered_at, reverse=True)
227
+ best = candidates[0]
228
+ platform = best.manifest.platform_hint or best.gateway_family
229
+ return Channel(
230
+ platform=platform,
231
+ chat_id=best.manifest.home_chat_id,
232
+ channel_type="home",
233
+ )
234
+ except Exception:
235
+ logger.debug("channel-store home resolution failed", exc_info=True)
236
+ return None
237
+
238
+ def reload(self) -> None:
239
+ """Re-read env vars and JSON config without restarting the sidecar.
240
+
241
+ This is a partial reload — contact handles are not re-fetched.
242
+ """
243
+ env_prefix = "COLONY_CHANNEL_"
244
+ self._env, self._json, self._fallback = self._load_sources(
245
+ json_path=self._json_path(),
246
+ env_prefix=env_prefix,
247
+ )
248
+ raw = os.environ.get("COLONY_OWNER_CONTACT_ID", "")
249
+ self._owner_contact_id = self._normalize_person_id(raw) if raw else None
250
+ logger.info("ChannelRegistry reloaded")
251
+
252
+ # ------------------------------------------------------------------
253
+ # Class methods
254
+ # ------------------------------------------------------------------
255
+
256
+ @classmethod
257
+ def load(
258
+ cls,
259
+ json_path: Optional[str] = None,
260
+ env_prefix: str = "COLONY_CHANNEL_",
261
+ handle_inference: Optional[bool] = None,
262
+ gateway_map: Optional[Dict[str, str]] = None,
263
+ contacts_store: Optional[Any] = None,
264
+ channel_store: Optional[Any] = None,
265
+ ) -> "ChannelRegistry":
266
+ """Load a ChannelRegistry from all configured sources.
267
+
268
+ Args:
269
+ json_path: Path to channels.json. Defaults to {COLONY_STATE_DIR}/data/channels.json.
270
+ env_prefix: Prefix for env var scanning.
271
+ handle_inference: Enable contact handle inference. Defaults to env var COLONY_CHANNEL_INFER_FROM_HANDLES.
272
+ gateway_map: Override gateway-to-platform mapping. Defaults to env var COLONY_CHANNEL_GATEWAY_MAP.
273
+ contacts_store: Optional contact store for handle inference.
274
+ """
275
+ if json_path is None:
276
+ state_dir = os.environ.get("COLONY_STATE_DIR", str(Path.home() / ".colony"))
277
+ json_path = os.path.join(state_dir, "data", "channels.json")
278
+
279
+ if handle_inference is None:
280
+ handle_inference = os.environ.get("COLONY_CHANNEL_INFER_FROM_HANDLES", "true").lower() == "true"
281
+
282
+ if gateway_map is None:
283
+ raw_map = os.environ.get("COLONY_CHANNEL_GATEWAY_MAP", "")
284
+ if raw_map:
285
+ try:
286
+ gateway_map = json.loads(raw_map)
287
+ except json.JSONDecodeError:
288
+ logger.warning("Invalid COLONY_CHANNEL_GATEWAY_MAP JSON, using defaults")
289
+ gateway_map = dict(cls.DEFAULT_GATEWAY_MAP)
290
+ else:
291
+ gateway_map = dict(cls.DEFAULT_GATEWAY_MAP)
292
+
293
+ # Normalize gateway_map
294
+ if gateway_map is None:
295
+ gateway_map = dict(cls.DEFAULT_GATEWAY_MAP)
296
+
297
+ env_channels, json_channels, fallback = cls._load_sources(json_path, env_prefix)
298
+
299
+ owner_contact_id = os.environ.get("COLONY_OWNER_CONTACT_ID", "")
300
+
301
+ registry = cls(
302
+ env_channels=env_channels,
303
+ json_channels=json_channels,
304
+ fallback_channels=fallback,
305
+ handle_inference=handle_inference,
306
+ gateway_map=gateway_map,
307
+ contacts_store=contacts_store,
308
+ owner_contact_id=owner_contact_id or None,
309
+ channel_store=channel_store,
310
+ )
311
+ registry._json_path_value = json_path # stash for reload()
312
+
313
+ logger.info(
314
+ "ChannelRegistry loaded: %d env entries, %d JSON entries, inference=%s",
315
+ sum(len(v) for v in env_channels.values()),
316
+ sum(len(v) for v in json_channels.values()),
317
+ handle_inference,
318
+ )
319
+ return registry
320
+
321
+ # ------------------------------------------------------------------
322
+ # Private helpers
323
+ # ------------------------------------------------------------------
324
+
325
+ @staticmethod
326
+ def _normalize_person_id(person_id: str) -> str:
327
+ """Normalize a person ID: lowercase, spaces → underscores."""
328
+ return person_id.lower().strip().replace(" ", "_")
329
+
330
+ @classmethod
331
+ def _load_sources(
332
+ cls,
333
+ json_path: str,
334
+ env_prefix: str,
335
+ ) -> tuple[Dict[str, Dict[str, Channel]], Dict[str, Dict[str, Channel]], Dict[str, Channel]]:
336
+ """Load channels from env vars and JSON file."""
337
+ env_channels = cls._load_from_env(env_prefix)
338
+ json_channels, fallback = cls._load_from_json(json_path)
339
+ return env_channels, json_channels, fallback
340
+
341
+ @classmethod
342
+ def _load_from_env(
343
+ cls,
344
+ prefix: str,
345
+ ) -> Dict[str, Dict[str, Channel]]:
346
+ """Scan env vars matching COLONY_CHANNEL_DM_* and COLONY_CHANNEL_HOME_*."""
347
+ channels: Dict[str, Dict[str, Channel]] = {}
348
+ dm_prefix = f"{prefix}DM_"
349
+ home_prefix = f"{prefix}HOME_"
350
+
351
+ for key, value in os.environ.items():
352
+ person_id: Optional[str] = None
353
+ channel_type: Optional[str] = None
354
+
355
+ if key.startswith(dm_prefix):
356
+ person_id = key[len(dm_prefix):]
357
+ channel_type = "dm"
358
+ elif key.startswith(home_prefix):
359
+ # HOME_ could be global (HOME) or per-person (HOME_owner)
360
+ remainder = key[len(home_prefix):]
361
+ if remainder:
362
+ person_id = remainder
363
+ else:
364
+ person_id = "__global__"
365
+ channel_type = "home"
366
+ elif key == f"{prefix}HOME":
367
+ person_id = "__global__"
368
+ channel_type = "home"
369
+ else:
370
+ continue
371
+
372
+ if not value:
373
+ continue
374
+
375
+ platform, chat_id = cls._parse_channel_value(value)
376
+ if platform and chat_id:
377
+ normalized_id = cls._normalize_person_id(person_id)
378
+ if normalized_id not in channels:
379
+ channels[normalized_id] = {}
380
+ channels[normalized_id][channel_type] = Channel(
381
+ platform=platform,
382
+ chat_id=chat_id,
383
+ channel_type=channel_type,
384
+ )
385
+
386
+ return channels
387
+
388
+ @classmethod
389
+ def _load_from_json(
390
+ cls,
391
+ path: str,
392
+ ) -> tuple[Dict[str, Dict[str, Channel]], Dict[str, Channel]]:
393
+ """Load channels from JSON config file."""
394
+ channels: Dict[str, Dict[str, Channel]] = {}
395
+ fallback: Dict[str, Channel] = {}
396
+
397
+ try:
398
+ if not Path(path).exists():
399
+ return channels, fallback
400
+ with open(path, "r") as f:
401
+ data = json.load(f)
402
+ except (json.JSONDecodeError, OSError) as exc:
403
+ logger.warning("Failed to load channels.json: %s", exc)
404
+ return channels, fallback
405
+
406
+ # contacts section
407
+ for person_id, person_channels in data.get("contacts", {}).items():
408
+ normalized_id = cls._normalize_person_id(person_id)
409
+ channels[normalized_id] = {}
410
+ for channel_type, info in person_channels.items():
411
+ platform = info.get("platform", "")
412
+ chat_id = info.get("chat_id", "")
413
+ if platform and chat_id:
414
+ channels[normalized_id][channel_type] = Channel(
415
+ platform=platform,
416
+ chat_id=chat_id,
417
+ channel_type=channel_type,
418
+ )
419
+
420
+ # fallback section
421
+ for channel_type, info in data.get("fallback", {}).items():
422
+ platform = info.get("platform", "")
423
+ chat_id = info.get("chat_id", "")
424
+ if platform and chat_id:
425
+ fallback[channel_type] = Channel(
426
+ platform=platform,
427
+ chat_id=chat_id,
428
+ channel_type=channel_type,
429
+ )
430
+
431
+ return channels, fallback
432
+
433
+ @classmethod
434
+ def _parse_channel_value(cls, value: str) -> tuple[Optional[str], Optional[str]]:
435
+ """Parse a channel value like 'telegram:@username' or 'whatsapp:+1555...'."""
436
+ if not value:
437
+ return None, None
438
+ if ":" in value:
439
+ platform, chat_id = value.split(":", 1)
440
+ return platform.strip().lower(), chat_id.strip()
441
+ # If no platform prefix, assume it's a chat ID and platform is unknown
442
+ return None, value.strip()
443
+
444
+ def _resolve_home_from_env(self) -> Optional[Channel]:
445
+ """Scan for {PLATFORM}_HOME_CHANNEL env vars."""
446
+ pattern = re.compile(r"^(\w+)_HOME_CHANNEL$")
447
+ for key in sorted(os.environ.keys()):
448
+ match = pattern.match(key)
449
+ if match:
450
+ platform = match.group(1).lower()
451
+ chat_id = os.environ[key]
452
+ if chat_id:
453
+ return Channel(
454
+ platform=platform,
455
+ chat_id=chat_id,
456
+ channel_type="home",
457
+ )
458
+ return None
459
+
460
+ def _infer_from_handles(self, person_id: str) -> Optional[Channel]:
461
+ """Infer DM channel from contact handles.
462
+
463
+ Gracefully skips when the store's ``get_handles`` is async and we're
464
+ called from a synchronous ``resolve()`` context (e.g. inside an
465
+ already-running event loop). In those cases env-vars / JSON config
466
+ should already have covered the owner.
467
+ """
468
+ if self._contacts_store is None:
469
+ return None
470
+
471
+ method = getattr(self._contacts_store, "get_handles", None)
472
+ if method is None:
473
+ return None
474
+
475
+ # Avoid creating an unawaited coroutine object — check asyncness first
476
+ if inspect.iscoroutinefunction(method):
477
+ logger.debug(
478
+ "Skipping handle inference for %s — async store called from sync context",
479
+ person_id,
480
+ )
481
+ return None
482
+
483
+ try:
484
+ handles = method(person_id)
485
+ except Exception:
486
+ return None
487
+
488
+ if not handles:
489
+ return None
490
+
491
+ for handle in handles:
492
+ gateway = getattr(handle, "gateway", "")
493
+ address = getattr(handle, "address", "")
494
+ if not gateway or not address:
495
+ continue
496
+
497
+ platform = self._gateway_map.get(gateway)
498
+ if not platform:
499
+ continue
500
+
501
+ # Normalize phone numbers
502
+ if platform in ("whatsapp", "signal"):
503
+ address = self._normalize_phone(address)
504
+
505
+ return Channel(
506
+ platform=platform,
507
+ chat_id=address,
508
+ channel_type="dm",
509
+ )
510
+
511
+ return None
512
+
513
+ @staticmethod
514
+ def _normalize_phone(address: str) -> str:
515
+ """Normalize a phone number address."""
516
+ # Strip spaces, ensure leading +
517
+ cleaned = address.strip().replace(" ", "").replace("-", "").replace("(", "").replace(")", "")
518
+ if cleaned.startswith("++"):
519
+ cleaned = cleaned[1:]
520
+ if not cleaned.startswith("+") and cleaned.isdigit():
521
+ cleaned = "+" + cleaned
522
+ return cleaned
523
+
524
+ def _json_path(self) -> str:
525
+ """Return the JSON config path (stashed during load)."""
526
+ return getattr(self, "_json_path_value", "")
@@ -0,0 +1,50 @@
1
+ """Initiative delivery classification.
2
+
3
+ Splits initiative *types* into two disjoint routing classes:
4
+
5
+ * **reach-out** -- user-facing initiatives whose natural outcome is a message
6
+ to a person (a follow-up, a relationship touch, an introduction...). These
7
+ are delivered through the guarded proactive-delivery path so the host agent
8
+ can compose and send them in its own voice, under the delivery rate limiter
9
+ and approval gates.
10
+ * **internal** -- everything else (self-maintenance, health, research
11
+ bookkeeping, data quality, capability gaps...). These are processed in-place
12
+ by the sidecar's own execution backend and never messaged to a person.
13
+
14
+ The reach-out set is a small, conservative default and is fully overridable
15
+ from the environment so a deployment can widen or narrow it without a code
16
+ change. No deployment-specific identifiers live here.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import os
22
+ from typing import FrozenSet
23
+
24
+ # Conservative default: types that inherently mean "contact a person".
25
+ # Anything not in this set is treated as internal / self-maintenance.
26
+ _DEFAULT_REACHOUT_TYPES: FrozenSet[str] = frozenset({
27
+ "follow_up",
28
+ "relationship",
29
+ "introduction",
30
+ "scheduling",
31
+ "commitment",
32
+ "calendar",
33
+ })
34
+
35
+ # Env override: comma-separated list of initiative types. When set (non-empty)
36
+ # it fully replaces the default set.
37
+ _ENV_VAR = "COLONY_REACHOUT_TYPES"
38
+
39
+
40
+ def reachout_types() -> FrozenSet[str]:
41
+ """Return the set of initiative types routed to proactive delivery."""
42
+ raw = os.environ.get(_ENV_VAR, "").strip()
43
+ if raw:
44
+ return frozenset(t.strip() for t in raw.split(",") if t.strip())
45
+ return _DEFAULT_REACHOUT_TYPES
46
+
47
+
48
+ def is_reachout(initiative_type: str) -> bool:
49
+ """True if this initiative type should be delivered to a person."""
50
+ return bool(initiative_type) and initiative_type in reachout_types()