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,116 @@
1
+ """MonitoringHandler — health-check probe jobs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ipaddress
6
+ import logging
7
+ import os
8
+ import time
9
+ from typing import Any, Dict
10
+ from urllib.parse import urlsplit
11
+
12
+ from apsimo.task_queue.handlers.base import JobHandler, Job
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def _host_allowlisted(host: str) -> bool:
18
+ entries = {
19
+ item.strip().lower()
20
+ for item in os.environ.get(
21
+ "COLONY_MONITORING_HOST_ALLOWLIST", ""
22
+ ).split(",")
23
+ if item.strip()
24
+ }
25
+ host = host.lower().rstrip(".")
26
+ return host in entries or any(
27
+ entry.startswith("*.") and host.endswith(entry[1:])
28
+ for entry in entries
29
+ )
30
+
31
+
32
+ def _unsafe_address(value: str) -> bool:
33
+ address = ipaddress.ip_address(value)
34
+ return not address.is_global
35
+
36
+
37
+ async def _validate_endpoint(url: str) -> str:
38
+ parsed = urlsplit(str(url or ""))
39
+ if parsed.scheme not in {"http", "https"} or not parsed.hostname:
40
+ raise ValueError("monitoring endpoint must be an http(s) URL")
41
+ if parsed.username is not None or parsed.password is not None:
42
+ raise ValueError("monitoring endpoint cannot contain credentials")
43
+ host = parsed.hostname.rstrip(".")
44
+ try:
45
+ address = str(ipaddress.ip_address(host))
46
+ except ValueError:
47
+ # DNS resolution followed by an ordinary HTTP client resolution is a
48
+ # rebinding TOCTOU. Hostnames therefore require an exact/operator
49
+ # wildcard allowlist entry; literal public IPs need no DNS at all.
50
+ if not _host_allowlisted(host):
51
+ raise ValueError(
52
+ "monitoring hostname must be explicitly configured in "
53
+ "COLONY_MONITORING_HOST_ALLOWLIST"
54
+ )
55
+ return url
56
+ if _unsafe_address(address) and not _host_allowlisted(host):
57
+ raise ValueError(
58
+ "monitoring endpoint resolves to a non-public address; configure "
59
+ "COLONY_MONITORING_HOST_ALLOWLIST for an intentional internal probe"
60
+ )
61
+ return url
62
+
63
+
64
+ class MonitoringHandler(JobHandler):
65
+ """Run a health-check probe.
66
+
67
+ Job payload:
68
+ endpoint (str): HTTP URL to GET.
69
+ expected_status (int, default 200): Expected HTTP status.
70
+ timeout_secs (float, default 10): Per-request timeout.
71
+
72
+ Returns:
73
+ {"metrics": {"status": int, "latency_ms": float}, "alerts": list[str]}
74
+ """
75
+
76
+ async def execute(self, job: Job) -> Dict[str, Any]:
77
+ try:
78
+ import aiohttp
79
+ except ImportError as exc:
80
+ raise RuntimeError(
81
+ "MonitoringHandler requires 'aiohttp'. Install with: pip install aiohttp"
82
+ ) from exc
83
+
84
+ url = await _validate_endpoint(job.payload["endpoint"])
85
+ expected = job.payload.get("expected_status", 200)
86
+ timeout = job.payload.get("timeout_secs", 10.0)
87
+
88
+ t0 = time.monotonic()
89
+ async with aiohttp.ClientSession() as session:
90
+ async with session.get(
91
+ url,
92
+ timeout=aiohttp.ClientTimeout(total=timeout),
93
+ allow_redirects=False,
94
+ ) as resp:
95
+ latency_ms = (time.monotonic() - t0) * 1000
96
+ alerts = []
97
+ if resp.status != expected:
98
+ alerts.append(
99
+ f"Expected status {expected}, got {resp.status}"
100
+ )
101
+ succeeded = not alerts
102
+ return {
103
+ "status": "completed" if succeeded else "failed",
104
+ "summary": (
105
+ f"probe returned expected HTTP {resp.status}"
106
+ if succeeded else "; ".join(alerts)
107
+ ),
108
+ "action_plane": {
109
+ "state": "completed" if succeeded else "failed",
110
+ },
111
+ "metrics": {
112
+ "status": resp.status,
113
+ "latency_ms": round(latency_ms, 2),
114
+ },
115
+ "alerts": alerts,
116
+ }
@@ -0,0 +1,75 @@
1
+ """Default handler registry for the Colony task queue."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any, Dict, Optional
6
+
7
+ from apsimo.task_queue.models import JobType
8
+ from apsimo.task_queue.worker import JobHandler
9
+ from apsimo.task_queue.handlers.monitoring import MonitoringHandler
10
+ from apsimo.task_queue.handlers.subtask_handler import SubtaskHandler
11
+ from apsimo.task_queue.handlers.system_maintenance import SystemMaintenanceHandler
12
+
13
+ if TYPE_CHECKING:
14
+ from apsimo.router.router import LLMRouter
15
+ from apsimo.world_model.store import WorldModelStore
16
+ from apsimo.contacts.store import ContactStore
17
+ from apsimo.task_queue.handlers.inference import _InferenceGateSessionStore
18
+
19
+
20
+ def build_default_handlers(
21
+ router: Optional["LLMRouter"] = None,
22
+ world_model_store: Optional["WorldModelStore"] = None,
23
+ contact_store: Optional["ContactStore"] = None,
24
+ response_gate: Optional[Any] = None,
25
+ gate_session_store: Optional["_InferenceGateSessionStore"] = None,
26
+ desktop_config: Optional[Any] = None,
27
+ browser_config: Optional[Any] = None,
28
+ node_id: str = "",
29
+ ) -> Dict[JobType, JobHandler]:
30
+ """Assemble the default set of job handlers.
31
+
32
+ Args:
33
+ router: LLMRouter instance. Required for INFERENCE jobs.
34
+ If None, InferenceHandler is omitted from the registry.
35
+ world_model_store: Optional WorldModelStore for context enrichment.
36
+ contact_store: Optional ContactStore for contact resolution.
37
+ response_gate: Optional ResponseGate for outbound response filtering.
38
+ gate_session_store: Session store adapter used by the ResponseGate.
39
+ desktop_config: Retired unsupported parameter; must be None.
40
+ browser_config: Retired unsupported parameter; must be None.
41
+ node_id: Legacy compatibility parameter, unused by supported handlers.
42
+
43
+ Returns:
44
+ Dict mapping JobType → JobHandler instance.
45
+ """
46
+ if desktop_config is not None or browser_config is not None:
47
+ raise ValueError(
48
+ "Desktop/browser queue workers are unsupported. Remove desktop_config "
49
+ "and browser_config; use the native runtime's registered tools."
50
+ )
51
+ handlers: Dict[JobType, JobHandler] = {
52
+ JobType.MONITORING: MonitoringHandler(),
53
+ JobType.SYSTEM_MAINTENANCE: SystemMaintenanceHandler(),
54
+ JobType.CUSTOM: SubtaskHandler(),
55
+ }
56
+
57
+ if router is not None:
58
+ from apsimo.task_queue.handlers.inference import (
59
+ InferenceHandler,
60
+ ThoughtOnlyInferenceHandler,
61
+ )
62
+ inference_handler = InferenceHandler(
63
+ router,
64
+ world_model_store=world_model_store,
65
+ contact_store=contact_store,
66
+ response_gate=response_gate,
67
+ gate_session_store=gate_session_store,
68
+ )
69
+ handlers[JobType.INFERENCE] = inference_handler
70
+ # ThoughtJobV1 shares only the router transport. A distinct strict
71
+ # handler makes it impossible for a registry mix-up to run a generic
72
+ # inference payload on the owner-private cognition lane.
73
+ handlers[JobType.THOUGHT] = ThoughtOnlyInferenceHandler(router)
74
+
75
+ return handlers
@@ -0,0 +1,173 @@
1
+ """Custom subtask handler for the Colony task queue.
2
+
3
+ Handles job_type=CUSTOM jobs dispatched by GoalQueueBridge.
4
+ For simple subtasks, spawns a lightweight Sonnet agent to execute the task
5
+ and returns the result as a job output dict.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import os
12
+ from datetime import datetime, timezone
13
+ from typing import Any, Dict
14
+
15
+ from apsimo.task_queue.models import Job
16
+ from apsimo.task_queue.worker import JobHandler
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ _SONNET_MODEL = "claude-sonnet-4-6"
21
+ _MAX_TOKENS = 1024
22
+
23
+ _SYSTEM_PROMPT = (
24
+ "You are an Apsimo subtask worker with NO tools: you cannot browse, run "
25
+ "commands, send messages, or touch external systems. Produce the work "
26
+ "product itself: the analysis, draft, plan, or answer the task asks for, "
27
+ "concise and self-contained. Never claim to have performed an external "
28
+ "action. If the task needs information or access you do not have, state "
29
+ "exactly what is missing in one line and deliver the best partial result "
30
+ "from what is given. Do not ask clarifying questions; make the most "
31
+ "reasonable assumption and note it."
32
+ )
33
+
34
+
35
+ class SubtaskHandler(JobHandler):
36
+ """Executes custom subtasks dispatched by GoalQueueBridge.
37
+
38
+ The job payload is expected to contain:
39
+ goal_id — parent goal identifier
40
+ subtask_id — subtask identifier
41
+ description — natural-language task description (preferred)
42
+ title / task / action / objective — fallback description keys
43
+
44
+ For each subtask the handler calls the Anthropic API with the Sonnet
45
+ model (restricted to text-only, no tools) and returns the model's
46
+ completion report as the job output.
47
+ """
48
+
49
+ def __init__(self, api_key: str | None = None) -> None:
50
+ self._api_key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
51
+
52
+ async def execute(self, job: Job) -> Dict[str, Any]:
53
+ goal_id = job.payload.get("goal_id", "")
54
+ subtask_id = job.payload.get("subtask_id", "")
55
+ task_description = job.payload.get("description", "") or _build_description(job.payload)
56
+
57
+ logger.info(
58
+ "SubtaskHandler: executing subtask %s for goal %s",
59
+ subtask_id,
60
+ goal_id,
61
+ )
62
+
63
+ result = await self._run_agent(task_description, goal_id, subtask_id)
64
+
65
+ logger.info(
66
+ "SubtaskHandler: subtask %s finished (goal=%s, success=%s)",
67
+ subtask_id,
68
+ goal_id,
69
+ result.get("success"),
70
+ )
71
+ return result
72
+
73
+ async def _run_agent(
74
+ self,
75
+ task_description: str,
76
+ goal_id: str,
77
+ subtask_id: str,
78
+ ) -> Dict[str, Any]:
79
+ """Call a lightweight Sonnet agent to execute the task."""
80
+ if not self._api_key:
81
+ logger.warning(
82
+ "SubtaskHandler: ANTHROPIC_API_KEY not set — subtask %s skipped",
83
+ subtask_id,
84
+ )
85
+ return {
86
+ "success": False,
87
+ "status": "failed",
88
+ "action_plane": {"state": "failed"},
89
+ "error": "ANTHROPIC_API_KEY not configured",
90
+ "goal_id": goal_id,
91
+ "subtask_id": subtask_id,
92
+ }
93
+
94
+ try:
95
+ import anthropic
96
+ except ImportError:
97
+ logger.error("SubtaskHandler: anthropic package not installed")
98
+ return {
99
+ "success": False,
100
+ "status": "failed",
101
+ "action_plane": {"state": "failed"},
102
+ "error": "anthropic package not available",
103
+ "goal_id": goal_id,
104
+ "subtask_id": subtask_id,
105
+ }
106
+
107
+ # Try ModelRegistry utility role for model/credentials; fall back to hardcoded
108
+ _model = _SONNET_MODEL
109
+ _api_key = self._api_key
110
+ try:
111
+ from colony_cli.model_config import load_model_registry
112
+ _registry = load_model_registry()
113
+ _rt = _registry.resolve_runtime(role="utility")
114
+ if _rt.get("model"):
115
+ _model = _rt["model"]
116
+ if _rt.get("api_key"):
117
+ _api_key = _rt["api_key"]
118
+ except Exception:
119
+ pass
120
+
121
+ client = anthropic.AsyncAnthropic(api_key=_api_key)
122
+ try:
123
+ message = await client.messages.create(
124
+ model=_model,
125
+ max_tokens=_MAX_TOKENS,
126
+ system=_SYSTEM_PROMPT,
127
+ messages=[
128
+ {
129
+ "role": "user",
130
+ "content": (
131
+ f"Goal ID: {goal_id}\n"
132
+ f"Subtask ID: {subtask_id}\n"
133
+ f"Task: {task_description}"
134
+ ),
135
+ }
136
+ ],
137
+ )
138
+ output_text = message.content[0].text if message.content else ""
139
+ return {
140
+ "success": True,
141
+ "status": "completed",
142
+ "summary": output_text,
143
+ "action_plane": {"state": "completed"},
144
+ "output": output_text,
145
+ "goal_id": goal_id,
146
+ "subtask_id": subtask_id,
147
+ "model": _model,
148
+ "completed_at": datetime.now(timezone.utc).isoformat(),
149
+ }
150
+ except Exception as exc:
151
+ logger.error(
152
+ "SubtaskHandler: agent call failed for subtask %s: %s",
153
+ subtask_id,
154
+ exc,
155
+ )
156
+ return {
157
+ "success": False,
158
+ "status": "failed",
159
+ "action_plane": {"state": "failed"},
160
+ "error": str(exc),
161
+ "goal_id": goal_id,
162
+ "subtask_id": subtask_id,
163
+ }
164
+
165
+
166
+ def _build_description(payload: Dict[str, Any]) -> str:
167
+ """Construct a task description from payload when 'description' is absent."""
168
+ for key in ("title", "task", "action", "objective"):
169
+ if payload.get(key):
170
+ return str(payload[key])
171
+ skip = {"goal_id", "subtask_id"}
172
+ parts = [f"{k}: {v}" for k, v in payload.items() if k not in skip and v]
173
+ return " — ".join(parts) or "Execute subtask"
@@ -0,0 +1,147 @@
1
+ """SystemMaintenanceHandler — safe allow-listed maintenance jobs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ import shutil
8
+ from pathlib import Path
9
+ from typing import Any, Dict, List
10
+
11
+ from apsimo.task_queue.handlers.base import JobHandler, Job, run_subprocess
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class SystemMaintenanceHandler(JobHandler):
17
+ """Run safe system maintenance commands (allow-listed only).
18
+
19
+ Job payload:
20
+ action (str): One of: "disk_cleanup", "log_rotate", "db_vacuum".
21
+ target_path (str, optional): Path scoped to the colony data directory.
22
+
23
+ Returns:
24
+ {"actions_taken": list[str], "errors": list[str]}
25
+ """
26
+
27
+ ALLOWED_ACTIONS = {"disk_cleanup", "log_rotate", "db_vacuum"}
28
+
29
+ def __init__(self, allowed_roots: List[Path] | None = None) -> None:
30
+ if allowed_roots is None:
31
+ configured = os.environ.get("COLONY_MAINTENANCE_ROOTS", "")
32
+ roots = [
33
+ Path(item.strip()).expanduser()
34
+ for item in configured.split(",") if item.strip()
35
+ ]
36
+ if not roots:
37
+ roots = [Path(
38
+ os.environ.get("COLONY_STATE_DIR")
39
+ or os.environ.get("COLONY_HOME", "~/.colony")
40
+ ).expanduser()]
41
+ else:
42
+ roots = list(allowed_roots)
43
+ self._allowed_roots = tuple(root.resolve() for root in roots)
44
+
45
+ def _target(self, value: str | None, *, default_name: str) -> Path:
46
+ root = self._allowed_roots[0]
47
+ raw = Path(value).expanduser() if value else root / default_name
48
+ if not raw.is_absolute():
49
+ raw = root / raw
50
+ resolved = raw.resolve(strict=False)
51
+ if not any(resolved.is_relative_to(base) for base in self._allowed_roots):
52
+ raise ValueError(
53
+ "maintenance target must stay inside COLONY_MAINTENANCE_ROOTS"
54
+ )
55
+ if resolved in self._allowed_roots:
56
+ raise ValueError("maintenance target cannot be a configured root")
57
+ return resolved
58
+
59
+ async def execute(self, job: Job) -> Dict[str, Any]:
60
+ action = job.payload.get("action")
61
+ if action not in self.ALLOWED_ACTIONS:
62
+ raise ValueError(
63
+ f"Disallowed maintenance action: {action!r}. "
64
+ f"Allowed: {sorted(self.ALLOWED_ACTIONS)}"
65
+ )
66
+
67
+ target_path: str | None = job.payload.get("target_path")
68
+ actions_taken: List[str] = []
69
+ errors: List[str] = []
70
+
71
+ if action == "db_vacuum":
72
+ if not target_path:
73
+ errors.append("db_vacuum requires 'target_path' in payload")
74
+ else:
75
+ try:
76
+ path = self._target(target_path, default_name="state.db")
77
+ import aiosqlite
78
+ async with aiosqlite.connect(path) as db:
79
+ await db.execute("VACUUM")
80
+ actions_taken.append(f"vacuumed {path}")
81
+ logger.info("SystemMaintenance: vacuumed %s", path)
82
+ except ImportError:
83
+ errors.append(
84
+ "db_vacuum requires 'aiosqlite'. Install with: pip install aiosqlite"
85
+ )
86
+ except Exception as exc:
87
+ errors.append(f"db_vacuum failed: {exc}")
88
+ logger.warning(
89
+ "SystemMaintenance: db_vacuum error on %s: %s",
90
+ target_path, exc,
91
+ )
92
+
93
+ elif action == "disk_cleanup":
94
+ path = self._target(target_path, default_name="tmp")
95
+ try:
96
+ cleaned = 0
97
+ if path.is_dir():
98
+ for item in path.iterdir():
99
+ try:
100
+ if item.is_symlink() or item.is_file():
101
+ item.unlink()
102
+ cleaned += 1
103
+ elif item.is_dir():
104
+ shutil.rmtree(item)
105
+ cleaned += 1
106
+ except Exception as exc:
107
+ errors.append(f"Could not remove {item}: {exc}")
108
+ actions_taken.append(f"disk_cleanup: removed {cleaned} items from {path}")
109
+ logger.info("SystemMaintenance: disk_cleanup removed %d items from %s", cleaned, path)
110
+ except Exception as exc:
111
+ errors.append(f"disk_cleanup failed: {exc}")
112
+ logger.warning("SystemMaintenance: disk_cleanup error: %s", exc)
113
+
114
+ elif action == "log_rotate":
115
+ path = self._target(target_path, default_name="logs")
116
+ try:
117
+ rotated = 0
118
+ if path.is_dir():
119
+ for log_file in path.glob("*.log"):
120
+ rotated_name = log_file.with_suffix(".log.bak")
121
+ try:
122
+ log_file.rename(rotated_name)
123
+ rotated += 1
124
+ except Exception as exc:
125
+ errors.append(f"Could not rotate {log_file}: {exc}")
126
+ actions_taken.append(f"log_rotate: rotated {rotated} log files in {path}")
127
+ logger.info("SystemMaintenance: log_rotate rotated %d files in %s", rotated, path)
128
+ except Exception as exc:
129
+ errors.append(f"log_rotate failed: {exc}")
130
+ logger.warning("SystemMaintenance: log_rotate error: %s", exc)
131
+
132
+ succeeded = not errors
133
+ return {
134
+ "status": "completed" if succeeded else "failed",
135
+ "summary": (
136
+ "; ".join(actions_taken)
137
+ if succeeded else "; ".join(errors)
138
+ ),
139
+ "operations": [
140
+ "delete" if action == "disk_cleanup" else "write"
141
+ ],
142
+ "actions_taken": actions_taken,
143
+ "errors": errors,
144
+ "action_plane": {
145
+ "state": "completed" if succeeded else "failed",
146
+ },
147
+ }
@@ -0,0 +1,111 @@
1
+ """Mesh layer integration for the Colony distributed task queue.
2
+
3
+ Bridges SWIM node-dead events and mesh role-change events into
4
+ queue scheduler actions. Wires the task queue into the existing
5
+ colony/mesh/ layer without modifying it.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from typing import Any, Optional
12
+
13
+ from apsimo.task_queue.queue_manager import QueueManager
14
+ from apsimo.task_queue.scheduler import Scheduler
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ _BACKGROUND_TASKS: set = set()
20
+
21
+
22
+ def _retain(task) -> None:
23
+ """Keep a strong reference to a fire-and-forget task until it finishes.
24
+
25
+ The event loop holds only a weak reference; an unreferenced task can be
26
+ garbage-collected (cancelled) mid-flight — for the Sovereign scheduler
27
+ loop that means job scheduling silently halts with no error.
28
+ """
29
+ _BACKGROUND_TASKS.add(task)
30
+ task.add_done_callback(_BACKGROUND_TASKS.discard)
31
+
32
+
33
+ class QueueMeshEventHandler:
34
+ """Handles mesh-layer events that affect the task queue.
35
+
36
+ Subscribe this handler to the Colony event bus for MeshEvent types.
37
+
38
+ Responsibilities:
39
+ - On NODE_DEAD: immediately abandon all jobs claimed by the dead node.
40
+ - On ROLE_CHANGE to SOVEREIGN (self): assume the scheduler role.
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ queue: QueueManager,
46
+ scheduler: Scheduler,
47
+ own_node_id: str,
48
+ event_bus: Optional[Any] = None,
49
+ ) -> None:
50
+ self._queue = queue
51
+ self._scheduler = scheduler
52
+ self._own_node_id = own_node_id
53
+ self._event_bus = event_bus
54
+
55
+ async def on_node_dead(self, node_id: str) -> None:
56
+ """Immediately abandon all jobs claimed by the dead node.
57
+
58
+ Called when SWIM emits a NODE_DEAD event. Does not wait for the
59
+ heartbeat timeout — fast-path abandonment.
60
+ """
61
+ abandoned = await self._queue.abandon_jobs_for_node(node_id)
62
+ if abandoned:
63
+ logger.info(
64
+ "QueueMeshEventHandler: abandoned %d jobs from dead node %s: %s",
65
+ len(abandoned), node_id, abandoned,
66
+ )
67
+ # Trigger immediate redistribution
68
+ await self._queue.requeue_retryable_jobs(
69
+ __import__("datetime").datetime.now(__import__("datetime").timezone.utc)
70
+ )
71
+
72
+ async def on_role_change(self, new_role: str, node_id: str) -> None:
73
+ """Handle mesh role-change events.
74
+
75
+ If this node becomes SOVEREIGN (Queen), start the scheduler loop.
76
+ """
77
+ if new_role == "sovereign" and node_id == self._own_node_id:
78
+ logger.info(
79
+ "QueueMeshEventHandler: this node (%s) became Sovereign — "
80
+ "starting scheduler", self._own_node_id,
81
+ )
82
+ import asyncio
83
+ _retain(asyncio.create_task(self._scheduler.run()))
84
+
85
+ def handle_mesh_event(self, event: Any) -> None:
86
+ """Synchronous event bus callback. Dispatches to async handlers."""
87
+ import asyncio
88
+ node_id = getattr(event, "node_id", "")
89
+ event_type = getattr(event, "event_type", "")
90
+
91
+ if event_type == "node_dead" or event_type == "NODE_DEAD":
92
+ _retain(asyncio.create_task(self.on_node_dead(node_id)))
93
+ elif event_type in {"role_changed", "ROLE_CHANGE"}:
94
+ new_role = ""
95
+ new_role_attr = getattr(event, "new_role", None)
96
+ if new_role_attr is not None:
97
+ new_role = str(new_role_attr.value if hasattr(new_role_attr, "value") else new_role_attr)
98
+ _retain(asyncio.create_task(self.on_role_change(new_role, node_id)))
99
+
100
+ def subscribe_to_event_bus(self) -> None:
101
+ """Register this handler on the Colony event bus if one is configured."""
102
+ if self._event_bus is None:
103
+ return
104
+ from apsimo.events.types import MeshEvent
105
+ self._event_bus.subscribe(
106
+ handler=self.handle_mesh_event,
107
+ event_types=[MeshEvent],
108
+ )
109
+ logger.info(
110
+ "QueueMeshEventHandler: subscribed to MeshEvent on event bus"
111
+ )