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,251 @@
1
+ """Candidate links and exact owner corrections in the existing contact store.
2
+
3
+ These functions establish attribution, never tool grants. The authenticated
4
+ host owns caller authorization and the source-ledger correction transaction.
5
+ """
6
+ import hashlib
7
+ import json
8
+
9
+
10
+ def _json(value):
11
+ return json.dumps(value, sort_keys=True, separators=(',', ':'), allow_nan=False)
12
+
13
+
14
+ def _refs(values):
15
+ if not isinstance(values, (list, tuple)) or len(values) > 100:
16
+ raise ValueError('invalid_identity_evidence')
17
+ if any(not isinstance(v, str) or not v.strip() or len(v) > 256 for v in values):
18
+ raise ValueError('invalid_identity_evidence')
19
+ return sorted(set(values))
20
+
21
+
22
+ def usable_handle(handle):
23
+ """Legacy name guesses are hypotheses, even before an operator migrates them."""
24
+ return bool(getattr(handle, 'verified', False)) or getattr(handle, 'source', '') != 'auto:scoped-name'
25
+
26
+
27
+ def normalized_handle(gateway, address):
28
+ from .store import _normalize_email, _normalize_phone
29
+ if not isinstance(gateway, str) or not isinstance(address, str):
30
+ raise ValueError('invalid_identity_handle')
31
+ gateway, address = gateway.strip().lower(), address.strip()
32
+ if not gateway or len(gateway) > 64 or not address or len(address) > 512:
33
+ raise ValueError('invalid_identity_handle')
34
+ if gateway == 'rcs':
35
+ gateway = 'sms'
36
+ if gateway == 'email':
37
+ address = _normalize_email(address)
38
+ elif gateway in ('sms', 'imessage', 'signal'):
39
+ address = _normalize_phone(address)
40
+ return gateway, address
41
+
42
+
43
+ async def propose(store, *, contact_id, gateway, address, evidence_refs=(), source='auto:scoped-name'):
44
+ from .store import _now_iso
45
+ gateway, address = normalized_handle(gateway, address)
46
+ refs = _refs(evidence_refs)
47
+ if await store.get(contact_id) is None:
48
+ raise ValueError('identity_contact_not_found')
49
+ if not isinstance(source, str) or not source or len(source) > 128:
50
+ raise ValueError('invalid_identity_source')
51
+ candidate_id = 'identity-candidate:' + hashlib.sha256(_json([contact_id, gateway, address]).encode()).hexdigest()
52
+ db = store._require_db()
53
+ await db.execute('''INSERT OR IGNORE INTO contact_identity_candidates
54
+ (candidate_id,contact_id,gateway,address,source,evidence_refs_json,created_at)
55
+ VALUES (?,?,?,?,?,?,?)''', (candidate_id, contact_id, gateway, address, source, _json(refs), _now_iso()))
56
+ await db.commit()
57
+ async with db.execute('SELECT * FROM contact_identity_candidates WHERE contact_id=? AND gateway=? AND address=?',
58
+ (contact_id, gateway, address)) as cursor:
59
+ result = dict(await cursor.fetchone())
60
+ result['evidence_refs'] = json.loads(result.pop('evidence_refs_json'))
61
+ result['authority_granted'] = False
62
+ return result
63
+
64
+
65
+ async def correct(store, *, operation_id, performed_by, gateway, address,
66
+ expected_contact_id, contact_id, evidence_refs, affected_source_ids=()):
67
+ """Move/remove one exact handle, preserving old attribution in a durable receipt.
68
+
69
+ expected_contact_id=None means no current handle is expected. contact_id=None
70
+ rejects candidate links and removes the explicitly expected handle, if any.
71
+ The host supplies and subsequently reconciles the exact affected source IDs.
72
+ A lost response may repeat the operation; changed input under its ID fails.
73
+ """
74
+ from .store import _gen_id, _now_iso
75
+ gateway, address = normalized_handle(gateway, address)
76
+ refs, sources = _refs(evidence_refs), _refs(affected_source_ids)
77
+ if not refs:
78
+ raise ValueError('identity_correction_requires_evidence')
79
+ for value in (operation_id, performed_by):
80
+ if not isinstance(value, str) or not value.strip() or len(value) > 256:
81
+ raise ValueError('invalid_identity_correction')
82
+ for value in (expected_contact_id, contact_id):
83
+ if value is not None and (not isinstance(value, str) or not value.startswith('cid-') or len(value) > 256):
84
+ raise ValueError('invalid_identity_contact')
85
+ request_hash = hashlib.sha256(_json([operation_id, performed_by, gateway, address,
86
+ expected_contact_id, contact_id, refs, sources]).encode()).hexdigest()
87
+ db = await store._open_provision_connection()
88
+ try:
89
+ await db.execute('BEGIN IMMEDIATE')
90
+ async with db.execute('SELECT * FROM contact_identity_operations WHERE operation_id=?', (operation_id,)) as cur:
91
+ prior = await cur.fetchone()
92
+ if prior:
93
+ if prior['request_sha256'] != request_hash:
94
+ raise ValueError('identity_operation_conflict')
95
+ await db.rollback()
96
+ return json.loads(prior['result_json'])
97
+ async with db.execute('SELECT * FROM contact_handles WHERE gateway=? AND address=?', (gateway, address)) as cur:
98
+ handle = await cur.fetchone()
99
+ old_id = handle['contact_id'] if handle else None
100
+ if old_id != expected_contact_id:
101
+ raise ValueError('identity_preimage_changed')
102
+ if contact_id is not None:
103
+ async with db.execute('SELECT contact_id FROM contacts WHERE contact_id=? AND deleted_at IS NULL', (contact_id,)) as cur:
104
+ if await cur.fetchone() is None:
105
+ raise ValueError('identity_contact_not_found')
106
+ now = _now_iso()
107
+ handle_id = handle['handle_id'] if handle else None
108
+ if contact_id is None:
109
+ if handle:
110
+ await db.execute('DELETE FROM contact_handles WHERE handle_id=?', (handle_id,))
111
+ elif handle:
112
+ await db.execute('''UPDATE contact_handles SET contact_id=?, verified=1,
113
+ confidence=1,source='owner_operator',is_primary=CASE WHEN contact_id=? THEN is_primary ELSE 0 END
114
+ WHERE handle_id=?''', (contact_id, contact_id, handle_id))
115
+ else:
116
+ handle_id = _gen_id('hdl')
117
+ await db.execute('''INSERT INTO contact_handles
118
+ (handle_id,contact_id,gateway,address,is_primary,verified,confidence,source,created_at)
119
+ VALUES (?,?,?,?,0,1,1,'owner_operator',?)''', (handle_id, contact_id, gateway, address, now))
120
+ await db.execute('''UPDATE contact_identity_candidates SET status=CASE WHEN contact_id=?
121
+ THEN 'confirmed' ELSE 'rejected' END,resolved_at=? WHERE gateway=? AND address=? AND status='pending' ''',
122
+ (contact_id, now, gateway, address))
123
+ result = {'schema': 'IdentityCorrectionV1', 'operation_id': operation_id,
124
+ 'old_contact_id': old_id, 'contact_id': contact_id, 'handle_id': handle_id,
125
+ 'gateway': gateway, 'address_sha256': hashlib.sha256(address.encode()).hexdigest(),
126
+ 'evidence_refs': refs, 'affected_source_ids': sources, 'recorded_at': now,
127
+ 'performed_by': performed_by, 'authority_granted': False,
128
+ 'source_reconciliation_required': bool(sources)}
129
+ for cid in sorted({cid for cid in (old_id, contact_id) if cid}):
130
+ await db.execute('''INSERT INTO contact_audit
131
+ (id,contact_id,action,detail,performed_by,created_at) VALUES (?,?,?,?,?,?)''',
132
+ (_gen_id('aud'), cid, 'identity_corrected', _json(result), performed_by, now))
133
+ await db.execute('INSERT INTO contact_identity_operations VALUES (?,?,?,?)',
134
+ (operation_id, request_hash, _json(result), now))
135
+ await db.commit()
136
+ except Exception:
137
+ await db.rollback()
138
+ raise
139
+ finally:
140
+ await db.close()
141
+ from apsimo.identity.resolver import reset_identity_resolver
142
+ reset_identity_resolver()
143
+ return result
144
+
145
+
146
+ async def evidence(store, contact_id):
147
+ if await store.get(contact_id) is None:
148
+ raise ValueError('identity_contact_not_found')
149
+ handles = await store.get_handles(contact_id)
150
+ db = store._require_db()
151
+ async with db.execute('SELECT * FROM contact_identity_candidates WHERE contact_id=? ORDER BY created_at DESC LIMIT 100', (contact_id,)) as cur:
152
+ candidates = [dict(row) for row in await cur.fetchall()]
153
+ for row in candidates:
154
+ row['evidence_refs'] = json.loads(row.pop('evidence_refs_json'))
155
+ async with db.execute('''SELECT result_json FROM contact_identity_operations
156
+ WHERE json_extract(result_json,'$.old_contact_id')=? OR json_extract(result_json,'$.contact_id')=?
157
+ ORDER BY created_at DESC,operation_id LIMIT 20''', (contact_id, contact_id)) as cur:
158
+ operations = [json.loads(row['result_json']) for row in await cur.fetchall()]
159
+ summaries = [{key: op[key] for key in ('operation_id', 'old_contact_id', 'contact_id', 'recorded_at',
160
+ 'source_reconciliation_required')} | {
161
+ 'source_reconciliation_status': op.get('source_reconciliation_status',
162
+ 'pending' if op['source_reconciliation_required'] else 'complete'),
163
+ 'source_count': len(op['affected_source_ids']), 'source_ids': op['affected_source_ids'][:10],
164
+ 'sources_truncated': len(op['affected_source_ids']) > 10,
165
+ 'conflict': op.get('source_reconciliation_conflict')}
166
+ for op in operations]
167
+ return {'contact_id': contact_id, 'handles': [h.to_dict() | {
168
+ 'identity_status': 'confirmed' if h.verified else 'observed' if usable_handle(h) else 'tentative',
169
+ 'usable_for_attribution': usable_handle(h)} for h in handles], 'candidates': candidates,
170
+ 'operations': summaries, 'authority_granted': False}
171
+
172
+
173
+ async def pending_reconciliations(store, *, limit=100):
174
+ """Recover unfinished cross-store corrections from their existing receipts."""
175
+ db = store._require_db()
176
+ async with db.execute('''SELECT result_json FROM contact_identity_operations
177
+ WHERE json_extract(result_json,'$.source_reconciliation_required')=1
178
+ AND coalesce(json_extract(result_json,'$.source_reconciliation_status'),'pending')!='conflicted'
179
+ ORDER BY created_at,operation_id LIMIT ?''', (max(1, min(int(limit), 500)),)) as cur:
180
+ return [json.loads(row['result_json']) for row in await cur.fetchall()]
181
+
182
+
183
+ async def mark_sources_reconciled(store, *, operation_id, source_result):
184
+ """Finish an exact source correction without introducing a second queue."""
185
+ db = await store._open_provision_connection()
186
+ try:
187
+ await db.execute('BEGIN IMMEDIATE')
188
+ async with db.execute('SELECT result_json FROM contact_identity_operations WHERE operation_id=?', (operation_id,)) as cur:
189
+ row = await cur.fetchone()
190
+ if row is None:
191
+ raise ValueError('identity_operation_not_found')
192
+ result = json.loads(row['result_json'])
193
+ if (source_result.get('schema') != 'SourceAttributionCorrectionV1'
194
+ or source_result.get('operation_id') != operation_id
195
+ or source_result.get('old_contact_id') != result['old_contact_id']
196
+ or source_result.get('contact_id') != result['contact_id']
197
+ or sorted(source_result.get('source_ids', [])) != sorted(result['affected_source_ids'])):
198
+ raise ValueError('identity_reconciliation_mismatch')
199
+ summary = {key: source_result[key] for key in ('operation_id', 'source_ids',
200
+ 'affected_source_ids', 'invalidated_source_ids', 'recorded_at')}
201
+ if not result['source_reconciliation_required']:
202
+ if result.get('source_reconciliation') != summary:
203
+ raise ValueError('identity_reconciliation_conflict')
204
+ await db.rollback()
205
+ return result
206
+ result['source_reconciliation_required'] = False
207
+ result['source_reconciliation_status'] = 'complete'
208
+ result.pop('source_reconciliation_conflict', None)
209
+ result['source_reconciliation'] = summary
210
+ await db.execute('UPDATE contact_identity_operations SET result_json=? WHERE operation_id=?',
211
+ (_json(result), operation_id))
212
+ await db.commit()
213
+ return result
214
+ except Exception:
215
+ await db.rollback()
216
+ raise
217
+ finally:
218
+ await db.close()
219
+
220
+
221
+ async def mark_sources_conflicted(store, *, operation_id, code):
222
+ """Retain a permanent conflict for inspection without retrying it forever."""
223
+ from .store import _now_iso
224
+ if code not in {'source_erased', 'source_attribution_preimage_changed'}:
225
+ raise ValueError('invalid_identity_reconciliation_conflict')
226
+ db = await store._open_provision_connection()
227
+ try:
228
+ await db.execute('BEGIN IMMEDIATE')
229
+ async with db.execute('SELECT result_json FROM contact_identity_operations WHERE operation_id=?', (operation_id,)) as cur:
230
+ row = await cur.fetchone()
231
+ if row is None:
232
+ raise ValueError('identity_operation_not_found')
233
+ result = json.loads(row['result_json'])
234
+ if not result['source_reconciliation_required']:
235
+ raise ValueError('identity_reconciliation_already_complete')
236
+ if result.get('source_reconciliation_status') == 'conflicted':
237
+ if result['source_reconciliation_conflict']['code'] != code:
238
+ raise ValueError('identity_reconciliation_conflict_changed')
239
+ await db.rollback()
240
+ return result
241
+ result['source_reconciliation_status'] = 'conflicted'
242
+ result['source_reconciliation_conflict'] = {'code': code, 'recorded_at': _now_iso()}
243
+ await db.execute('UPDATE contact_identity_operations SET result_json=? WHERE operation_id=?',
244
+ (_json(result), operation_id))
245
+ await db.commit()
246
+ return result
247
+ except Exception:
248
+ await db.rollback()
249
+ raise
250
+ finally:
251
+ await db.close()
@@ -0,0 +1,280 @@
1
+ """Colony Contacts — import pipeline."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import csv
6
+ import hashlib
7
+ import io
8
+ import logging
9
+ import re
10
+ from abc import ABC, abstractmethod
11
+ from typing import List, Optional
12
+
13
+ from .config import ContactsConfig
14
+ from .importers.batch import BatchImportResult, ImportOutcome, ImportRecord
15
+ from .importers.macos_contacts import RawContactRecord
16
+ from .store import SQLiteContactStore, _normalize_phone, _normalize_email, _gen_id
17
+
18
+ logger = logging.getLogger("colony.contacts.importer")
19
+
20
+
21
+ def _pii_hash(value: Optional[str]) -> str:
22
+ """Short hash for PII-safe log correlation (never log raw contact data)."""
23
+ if not value:
24
+ return "∅"
25
+ return hashlib.sha256(value.encode("utf-8")).hexdigest()[:8]
26
+
27
+ # vCard property pattern
28
+ _VCARD_PROP = re.compile(r'^([A-Z\-]+)(?:;[^:]*)?:(.*)$')
29
+
30
+
31
+ def _parse_vcard(vcard_data: str) -> List[RawContactRecord]:
32
+ """Parse one or more vCard 4.0 entries into RawContactRecord objects."""
33
+ records: List[RawContactRecord] = []
34
+ current: Optional[dict] = None
35
+ for line in vcard_data.splitlines():
36
+ line = line.strip()
37
+ if line.upper() == "BEGIN:VCARD":
38
+ current = {"phones": [], "emails": [], "given": None, "family": None, "org": None, "fn": None}
39
+ elif line.upper() == "END:VCARD" and current is not None:
40
+ given = current["given"]
41
+ family = current["family"]
42
+ display = current["fn"] or (" ".join(p for p in [given, family] if p) or None)
43
+ if not given and display:
44
+ parts = display.split(None, 1)
45
+ given = parts[0] if parts else None
46
+ family = parts[1] if len(parts) > 1 else None
47
+ records.append(RawContactRecord(
48
+ given_name=given,
49
+ family_name=family,
50
+ organization=current["org"],
51
+ phone_numbers=current["phones"],
52
+ email_addresses=current["emails"],
53
+ source="vcard",
54
+ ))
55
+ current = None
56
+ elif current is not None:
57
+ m = _VCARD_PROP.match(line)
58
+ if not m:
59
+ continue
60
+ prop, val = m.group(1).upper(), m.group(2).strip()
61
+ if prop == "FN":
62
+ current["fn"] = val
63
+ elif prop == "N":
64
+ parts = val.split(";")
65
+ current["family"] = parts[0] if parts else None
66
+ current["given"] = parts[1] if len(parts) > 1 else None
67
+ elif prop in ("TEL", "X-TEL") or prop.startswith("TEL"):
68
+ if val:
69
+ current["phones"].append(val)
70
+ elif prop == "EMAIL":
71
+ if val:
72
+ current["emails"].append(val.lower())
73
+ elif prop == "ORG":
74
+ current["org"] = val
75
+ return records
76
+
77
+
78
+ def _parse_csv(csv_data: str, source: str) -> List[RawContactRecord]:
79
+ """Parse a CSV string into RawContactRecord objects."""
80
+ records: List[RawContactRecord] = []
81
+ reader = csv.DictReader(io.StringIO(csv_data))
82
+ for row in reader:
83
+ # Support common column names
84
+ given = row.get("given_name") or row.get("First Name") or row.get("first_name") or None
85
+ family = row.get("family_name") or row.get("Last Name") or row.get("last_name") or None
86
+ org = row.get("organization") or row.get("Organization") or row.get("Company") or None
87
+ phone_raw = row.get("phone") or row.get("Phone") or row.get("primary_phone") or ""
88
+ email_raw = row.get("email") or row.get("Email") or row.get("primary_email") or ""
89
+ phones = [p.strip() for p in phone_raw.split("|") if p.strip()] if phone_raw else []
90
+ emails = [e.strip().lower() for e in email_raw.split("|") if e.strip()] if email_raw else []
91
+ if not given and not family and not phones and not emails:
92
+ continue
93
+ records.append(RawContactRecord(
94
+ given_name=given or None,
95
+ family_name=family or None,
96
+ organization=org or None,
97
+ phone_numbers=phones,
98
+ email_addresses=emails,
99
+ source=source,
100
+ ))
101
+ return records
102
+
103
+
104
+ # ── Abstract interface ────────────────────────────────────────────────────────
105
+
106
+ class ContactImporter(ABC):
107
+ """Orchestrates contact import from any source through deduplication."""
108
+
109
+ @abstractmethod
110
+ async def import_raw(self, records: List[RawContactRecord], source: str) -> BatchImportResult:
111
+ """Import a list of RawContactRecord objects through dedup pipeline."""
112
+
113
+ @abstractmethod
114
+ async def import_from_macos_contacts(self) -> BatchImportResult:
115
+ """Import all contacts from the macOS Contacts framework."""
116
+
117
+ @abstractmethod
118
+ async def import_from_vcard(self, vcard_data: str) -> BatchImportResult:
119
+ """Import contacts from a vCard 4.0 string."""
120
+
121
+ @abstractmethod
122
+ async def import_from_csv(self, csv_data: str, source: str = "csv") -> BatchImportResult:
123
+ """Import contacts from a UTF-8 CSV string."""
124
+
125
+
126
+ # ── SQLite-backed implementation ──────────────────────────────────────────────
127
+
128
+ class SQLiteContactImporter(ContactImporter):
129
+ """ContactImporter backed by SQLiteContactStore."""
130
+
131
+ def __init__(
132
+ self,
133
+ store: SQLiteContactStore,
134
+ config: Optional[ContactsConfig] = None,
135
+ bridge = None, # Optional[WorldModelContactBridge]
136
+ ) -> None:
137
+ self._store = store
138
+ self._config = config or ContactsConfig()
139
+ self._bridge = bridge
140
+
141
+ async def import_raw(self, records: List[RawContactRecord], source: str) -> BatchImportResult:
142
+ result = BatchImportResult(source=source, total=len(records))
143
+ for raw in records:
144
+ display_name = raw.display_name
145
+ try:
146
+ # Normalize addresses
147
+ norm_phones = [_normalize_phone(p) for p in raw.phone_numbers if p]
148
+ norm_emails = [_normalize_email(e) for e in raw.email_addresses if e]
149
+
150
+ # Run dedup check
151
+ candidates = await self._store.find_dedup_candidates(
152
+ raw.given_name, raw.family_name, norm_phones, norm_emails
153
+ )
154
+
155
+ auto_threshold = self._config.auto_merge_confidence_threshold
156
+ proposal_threshold = self._config.merge_proposal_threshold
157
+
158
+ async def _attach(contact_id: str, kind: str, value: str, confidence: Optional[float]) -> None:
159
+ try:
160
+ kwargs = {"source": raw.source}
161
+ if confidence is not None:
162
+ kwargs["confidence"] = confidence
163
+ await self._store.add_handle(contact_id, kind, value, **kwargs)
164
+ except ValueError:
165
+ # Handle already owned by a different contact; store
166
+ # resolves ownership separately. Count the conflict
167
+ # so callers see lossy merges, and log a redacted
168
+ # correlation hash only — never raw PII.
169
+ result.handle_conflicts += 1
170
+ logger.debug(
171
+ "contact handle conflict: kind=%s value_hash=%s name_hash=%s",
172
+ kind, _pii_hash(value), _pii_hash(display_name),
173
+ )
174
+
175
+ if candidates and candidates[0][0] >= auto_threshold:
176
+ # High-confidence match — merge handles into existing contact
177
+ conf, existing_id, reason = candidates[0]
178
+ for phone in norm_phones:
179
+ await _attach(existing_id, "imessage", phone, conf)
180
+ for email in norm_emails:
181
+ await _attach(existing_id, "email", email, conf)
182
+ result.merged += 1
183
+ result.records.append(ImportRecord(
184
+ raw_display_name=display_name,
185
+ outcome=ImportOutcome.MERGED,
186
+ contact_id=existing_id,
187
+ merged_into_id=existing_id,
188
+ ))
189
+ # Absorb any discovered contacts with overlapping handles
190
+ if self._bridge:
191
+ await self._absorb_discovered_for(existing_id, norm_phones, norm_emails)
192
+ elif candidates and candidates[0][0] >= proposal_threshold:
193
+ # Below auto-merge but above proposal threshold — create and propose
194
+ contact = await self._store.create(
195
+ display_name=display_name,
196
+ given_name=raw.given_name,
197
+ family_name=raw.family_name,
198
+ organization=raw.organization,
199
+ import_source=raw.source,
200
+ )
201
+ for phone in norm_phones:
202
+ await _attach(contact.contact_id, "imessage", phone, None)
203
+ for email in norm_emails:
204
+ await _attach(contact.contact_id, "email", email, None)
205
+ # Queue merge proposal (handled by merger)
206
+ result.created += 1
207
+ result.records.append(ImportRecord(
208
+ raw_display_name=display_name,
209
+ outcome=ImportOutcome.CREATED,
210
+ contact_id=contact.contact_id,
211
+ ))
212
+ if self._bridge:
213
+ await self._absorb_discovered_for(contact.contact_id, norm_phones, norm_emails)
214
+ else:
215
+ # No match — create new contact
216
+ contact = await self._store.create(
217
+ display_name=display_name,
218
+ given_name=raw.given_name,
219
+ family_name=raw.family_name,
220
+ organization=raw.organization,
221
+ import_source=raw.source,
222
+ )
223
+ for phone in norm_phones:
224
+ await _attach(contact.contact_id, "imessage", phone, None)
225
+ for email in norm_emails:
226
+ await _attach(contact.contact_id, "email", email, None)
227
+ result.created += 1
228
+ result.records.append(ImportRecord(
229
+ raw_display_name=display_name,
230
+ outcome=ImportOutcome.CREATED,
231
+ contact_id=contact.contact_id,
232
+ ))
233
+ if self._bridge:
234
+ await self._absorb_discovered_for(contact.contact_id, norm_phones, norm_emails)
235
+ except Exception as exc:
236
+ logger.warning(
237
+ "Failed to import record (name_hash=%s): %s",
238
+ _pii_hash(display_name), type(exc).__name__,
239
+ )
240
+ result.failed += 1
241
+ result.records.append(ImportRecord(
242
+ raw_display_name=display_name,
243
+ outcome=ImportOutcome.FAILED,
244
+ error=type(exc).__name__,
245
+ ))
246
+ return result
247
+
248
+ async def _absorb_discovered_for(
249
+ self,
250
+ curated_contact_id: str,
251
+ norm_phones: List[str],
252
+ norm_emails: List[str],
253
+ ) -> None:
254
+ """Find discovered contacts with overlapping handles and absorb them."""
255
+ if not self._bridge:
256
+ return
257
+ for phone in norm_phones:
258
+ discovered = await self._store.find_discovered_by_handle("imessage", phone)
259
+ if discovered:
260
+ await self._bridge.absorb_discovered_contact(curated_contact_id, discovered)
261
+ return
262
+ for email in norm_emails:
263
+ discovered = await self._store.find_discovered_by_handle("email", email)
264
+ if discovered:
265
+ await self._bridge.absorb_discovered_contact(curated_contact_id, discovered)
266
+ return
267
+
268
+ async def import_from_macos_contacts(self) -> BatchImportResult:
269
+ from .importers.macos_contacts import MacOSContactsImporter
270
+ importer = MacOSContactsImporter(normalize_phones=self._config.import_cfg.normalize_phones)
271
+ raw_records = importer.fetch_all()
272
+ return await self.import_raw(raw_records, "ios_contacts")
273
+
274
+ async def import_from_vcard(self, vcard_data: str) -> BatchImportResult:
275
+ records = _parse_vcard(vcard_data)
276
+ return await self.import_raw(records, "vcard")
277
+
278
+ async def import_from_csv(self, csv_data: str, source: str = "csv") -> BatchImportResult:
279
+ records = _parse_csv(csv_data, source)
280
+ return await self.import_raw(records, source)
@@ -0,0 +1 @@
1
+ """Colony Contacts importers."""
@@ -0,0 +1,43 @@
1
+ """Batch import result types."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from enum import Enum
7
+ from typing import List, Optional
8
+
9
+
10
+ class ImportOutcome(str, Enum):
11
+ CREATED = "created"
12
+ MERGED = "merged"
13
+ SKIPPED = "skipped"
14
+ FAILED = "failed"
15
+
16
+
17
+ @dataclass
18
+ class ImportRecord:
19
+ """Result for a single record in a batch import."""
20
+ raw_display_name: Optional[str]
21
+ outcome: ImportOutcome
22
+ contact_id: Optional[str] = None # set on CREATED or MERGED
23
+ merged_into_id: Optional[str] = None # set on MERGED
24
+ error: Optional[str] = None # set on FAILED
25
+
26
+
27
+ @dataclass
28
+ class BatchImportResult:
29
+ """Summary of a completed batch import operation."""
30
+ source: str
31
+ total: int
32
+ created: int = 0
33
+ merged: int = 0
34
+ skipped: int = 0
35
+ failed: int = 0
36
+ handle_conflicts: int = 0
37
+ records: List[ImportRecord] = field(default_factory=list)
38
+
39
+ @property
40
+ def success_rate(self) -> float:
41
+ if self.total == 0:
42
+ return 1.0
43
+ return (self.created + self.merged + self.skipped) / self.total
@@ -0,0 +1,101 @@
1
+ """macOS Contacts framework importer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from dataclasses import dataclass, field
7
+ from typing import List, Optional
8
+
9
+ logger = logging.getLogger("colony.contacts.importers.macos_contacts")
10
+
11
+
12
+ @dataclass
13
+ class RawContactRecord:
14
+ """A contact record extracted from the macOS Contacts framework.
15
+
16
+ Intermediate representation before deduplication and persistence.
17
+ """
18
+ given_name: Optional[str]
19
+ family_name: Optional[str]
20
+ organization: Optional[str]
21
+ phone_numbers: List[str] = field(default_factory=list) # E.164-normalized where possible
22
+ email_addresses: List[str] = field(default_factory=list)
23
+ source: str = "ios_contacts"
24
+
25
+ @property
26
+ def display_name(self) -> Optional[str]:
27
+ parts = [self.given_name, self.family_name]
28
+ name = " ".join(p for p in parts if p)
29
+ return name or self.organization or None
30
+
31
+
32
+ class MacOSContactsImporter:
33
+ """Imports contacts from the macOS Contacts framework.
34
+
35
+ Requires pyobjc-framework-Contacts and macOS Contacts permission.
36
+ If permission is denied or the framework is unavailable, raises ImportError
37
+ with a descriptive message rather than silently returning an empty list.
38
+
39
+ Args:
40
+ normalize_phones: If True, attempt E.164 normalization of phone numbers.
41
+ """
42
+
43
+ def __init__(self, normalize_phones: bool = True) -> None:
44
+ self._normalize = normalize_phones
45
+ self._cn = self._load_framework()
46
+
47
+ def _load_framework(self):
48
+ try:
49
+ import Contacts # type: ignore # pyobjc
50
+ return Contacts
51
+ except ImportError as exc:
52
+ raise ImportError(
53
+ "pyobjc-framework-Contacts is required for macOS Contacts import. "
54
+ "Install it with: pip install pyobjc-framework-Contacts"
55
+ ) from exc
56
+
57
+ def fetch_all(self) -> List[RawContactRecord]:
58
+ """Fetch all contacts from the macOS Contacts store.
59
+
60
+ Returns:
61
+ List of RawContactRecord objects.
62
+
63
+ Raises:
64
+ PermissionError: If Contacts access has not been granted.
65
+ ImportError: If the Contacts framework is unavailable.
66
+ """
67
+ CN = self._cn
68
+ store = CN.CNContactStore.alloc().init()
69
+ keys = [
70
+ CN.CNContactGivenNameKey,
71
+ CN.CNContactFamilyNameKey,
72
+ CN.CNContactOrganizationNameKey,
73
+ CN.CNContactPhoneNumbersKey,
74
+ CN.CNContactEmailAddressesKey,
75
+ ]
76
+ fetch_request = CN.CNContactFetchRequest.alloc().initWithKeysToFetch_(keys)
77
+ records: List[RawContactRecord] = []
78
+
79
+ def handler(contact, _stop):
80
+ phones = [
81
+ v.value().stringValue()
82
+ for v in (contact.phoneNumbers() or [])
83
+ ]
84
+ emails = [
85
+ str(v.value())
86
+ for v in (contact.emailAddresses() or [])
87
+ ]
88
+ records.append(RawContactRecord(
89
+ given_name=str(contact.givenName()) or None,
90
+ family_name=str(contact.familyName()) or None,
91
+ organization=str(contact.organizationName()) or None,
92
+ phone_numbers=phones,
93
+ email_addresses=emails,
94
+ ))
95
+
96
+ error = None
97
+ store.enumerateContactsWithFetchRequest_error_usingBlock_(
98
+ fetch_request, error, handler
99
+ )
100
+ logger.info("Fetched %d contacts from macOS Contacts", len(records))
101
+ return records