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,123 @@
1
+ """Graduated approval policy helpers (v0.18.0) — outbound target checks.
2
+
3
+ The owner's policy: nothing waits on a manual approval unless it is
4
+ potentially destructive or an outreach to an individual the owner hasn't
5
+ authorized yet. The destructive half lives in the action registry's risk
6
+ tiers; this module answers the other half — *who* an OUTBOUND action
7
+ reaches, and whether that person is an authorized contact.
8
+
9
+ An OUTBOUND :class:`~apsimo.initiatives.action_registry.ActionSpec`
10
+ names the param holding its recipient via ``target_param``. The recipient
11
+ value is resolved against the contact store:
12
+
13
+ - ``gateway:address`` forms (``email:bob@x.com``, ``imessage:+1555...``)
14
+ resolve via ``resolve_handle``;
15
+ - ``cid-...`` resolves via ``get``;
16
+ - bare emails resolve via ``resolve_handle("email", ...)``;
17
+ - anything else falls back to ``find_by_name`` — and only an
18
+ unambiguous match counts (names are not unique; an ambiguous name must
19
+ not authorize the wrong person).
20
+
21
+ The check fails closed: missing store, missing/unresolvable target, or a
22
+ contact with ``interaction_allowed=False`` all deny auto-approval.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import logging
28
+ from typing import Any, Dict, Optional, Tuple
29
+
30
+ from apsimo.contacts.models import GATEWAYS
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+ def _extract_target(params: Optional[Dict[str, Any]], spec: Any) -> Optional[str]:
36
+ """Pull the recipient value named by ``spec.target_param``.
37
+
38
+ Looks in the job params themselves, then the nested ``context`` and
39
+ ``params`` dicts (the autonomy loop carries initiative context under
40
+ ``context``).
41
+ """
42
+ key = getattr(spec, "target_param", None) if spec is not None else None
43
+ if not key or not isinstance(params, dict):
44
+ return None
45
+ containers = (params, params.get("context"), params.get("params"))
46
+ for container in containers:
47
+ if not isinstance(container, dict):
48
+ continue
49
+ value = container.get(key)
50
+ if value:
51
+ return str(value).strip()
52
+ return None
53
+
54
+
55
+ async def _resolve_contact(target: str, contacts_store: Any) -> Optional[Any]:
56
+ """Resolve a recipient identifier to a Contact, or None."""
57
+ # gateway:address form (email:bob@x.com, imessage:+15551234567, ...)
58
+ if ":" in target:
59
+ gateway, _, address = target.partition(":")
60
+ gateway = gateway.strip().lower()
61
+ address = address.strip()
62
+ if gateway in GATEWAYS and address:
63
+ return await contacts_store.resolve_handle(gateway, address)
64
+
65
+ # Contact ID — primary key lookup.
66
+ if target.startswith("cid-"):
67
+ return await contacts_store.get(target)
68
+
69
+ # Bare email handle.
70
+ if "@" in target:
71
+ return await contacts_store.resolve_handle("email", target)
72
+
73
+ # Display name — only when unambiguous (mirrors IdentityResolver).
74
+ matches = await contacts_store.find_by_name(target, threshold=0.9)
75
+ wanted = target.lower()
76
+ exact = [
77
+ m for m in matches
78
+ if (getattr(m, "display_name", "") or "").strip().lower() == wanted
79
+ ]
80
+ candidates = exact or matches
81
+ if len(candidates) == 1:
82
+ return candidates[0]
83
+ if len(candidates) > 1:
84
+ logger.warning(
85
+ "Ambiguous outbound target %r: %d contacts match — refusing "
86
+ "to authorize", target, len(candidates),
87
+ )
88
+ return None
89
+
90
+
91
+ async def is_authorized_target(
92
+ params: Optional[Dict[str, Any]],
93
+ spec: Any,
94
+ contacts_store: Any,
95
+ ) -> Tuple[bool, str]:
96
+ """Is the recipient of an OUTBOUND action an authorized contact?
97
+
98
+ Returns ``(True, "contact:<cid>")`` only when the recipient named by
99
+ ``spec.target_param`` resolves to a contact with
100
+ ``interaction_allowed=True``. Everything else fails closed:
101
+
102
+ - no contact store wired → ``(False, "no_contact_store")``
103
+ - missing/unresolvable target → ``(False, "unknown_target")``
104
+ - contact found but not authorized → ``(False, "contact_not_authorized")``
105
+ """
106
+ if contacts_store is None:
107
+ return False, "no_contact_store"
108
+
109
+ target = _extract_target(params, spec)
110
+ if not target:
111
+ return False, "unknown_target"
112
+
113
+ try:
114
+ contact = await _resolve_contact(target, contacts_store)
115
+ except Exception as exc: # resolution must never break dispatch
116
+ logger.warning("Outbound target resolution failed for %r: %s", target, exc)
117
+ return False, "unknown_target"
118
+
119
+ if contact is None:
120
+ return False, "unknown_target"
121
+ if not getattr(contact, "interaction_allowed", False):
122
+ return False, "contact_not_authorized"
123
+ return True, f"contact:{getattr(contact, 'contact_id', 'unknown')}"
@@ -0,0 +1,263 @@
1
+ """Assignment Engine for multi-agent Colony.
2
+
3
+ Provides:
4
+ - Agent selection for initiatives
5
+ - Capability matching
6
+ - Load balancing
7
+ - Priority handling
8
+ """
9
+
10
+ import logging
11
+ from datetime import datetime, timedelta, timezone
12
+ from typing import Any, Dict, List, Optional
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ # Initiative type → required capabilities
18
+ INITIATIVE_CAPABILITIES: Dict[str, List[str]] = {
19
+ "follow_up": [], # Any agent
20
+ "relationship": ["messaging"], # Needs messaging
21
+ "scheduling": ["calendar"], # Needs calendar
22
+ "coding": ["coding"], # Needs code execution
23
+ "health": [], # Any agent
24
+ }
25
+
26
+ # Types that should prefer primary agent
27
+ USER_FACING_TYPES = ["follow_up", "relationship"]
28
+
29
+ # Default for unknown types
30
+ UNKNOWN_TYPE_POLICY = "allow_any" # "reject", "log_and_allow", "allow_any"
31
+
32
+
33
+ class AssignmentEngine:
34
+ """Selects the best agent for an initiative."""
35
+
36
+ def __init__(
37
+ self,
38
+ agent_store: Any,
39
+ initiative_store: Any,
40
+ unknown_type_policy: str = UNKNOWN_TYPE_POLICY,
41
+ ):
42
+ self._agent_store = agent_store
43
+ self._initiative_store = initiative_store
44
+ self._unknown_type_policy = unknown_type_policy
45
+
46
+ def select_agent(
47
+ self,
48
+ initiative: Any,
49
+ agents: Optional[List[Any]] = None,
50
+ ) -> Optional[Any]:
51
+ """Select the best agent for an initiative.
52
+
53
+ Priority:
54
+ 1. Online status
55
+ 2. Preferred agent (if specified)
56
+ 3. Capability match
57
+ 4. Type restrictions
58
+ 5. Primary designation (for user-facing)
59
+ 6. Load balancing
60
+ 7. Capacity check
61
+
62
+ Args:
63
+ initiative: Initiative to assign (StoredInitiative or dict)
64
+ agents: List of agents to choose from (if None, loads from store)
65
+
66
+ Returns:
67
+ Selected Agent or None if no suitable agent found
68
+ """
69
+ # Get initiative type
70
+ init_type = self._get_initiative_type(initiative)
71
+
72
+ # Check if type is known
73
+ if init_type not in INITIATIVE_CAPABILITIES:
74
+ if self._unknown_type_policy == "reject":
75
+ logger.warning("Unknown initiative type: %s, rejecting", init_type)
76
+ return None
77
+ elif self._unknown_type_policy == "log_and_allow":
78
+ logger.warning("Unknown initiative type: %s, allowing any agent", init_type)
79
+
80
+ # Get agents if not provided
81
+ if agents is None:
82
+ agents = self._agent_store.list(status=["online"])
83
+
84
+ # Step 1: Only online agents
85
+ candidates = [a for a in agents if a.status == "online"]
86
+ if not candidates:
87
+ return None
88
+
89
+ # Step 2: Preferred agent (if specified)
90
+ preferred_id = self._get_preferred_agent_id(initiative)
91
+ if preferred_id:
92
+ preferred = next(
93
+ (a for a in candidates if a.agent_id == preferred_id),
94
+ None
95
+ )
96
+ if preferred and self._has_capacity(preferred):
97
+ return preferred
98
+
99
+ # Step 3: Capability filter
100
+ required_caps = INITIATIVE_CAPABILITIES.get(init_type, [])
101
+ if required_caps:
102
+ candidates = [
103
+ a for a in candidates
104
+ if a.has_capabilities(required_caps)
105
+ ]
106
+
107
+ if not candidates:
108
+ logger.debug(
109
+ "No agents with capabilities %s for initiative type %s",
110
+ required_caps,
111
+ init_type,
112
+ )
113
+ return None
114
+
115
+ # Step 4: Type restrictions (excluded/included types)
116
+ filtered = []
117
+ for agent in candidates:
118
+ if not agent.can_handle_type(init_type):
119
+ continue
120
+ filtered.append(agent)
121
+
122
+ candidates = filtered if filtered else candidates
123
+
124
+ if not candidates:
125
+ return None
126
+
127
+ # Step 5: Primary preference for user-facing
128
+ if init_type in USER_FACING_TYPES:
129
+ primaries = [a for a in candidates if a.is_primary]
130
+ if primaries:
131
+ candidates = primaries
132
+
133
+ # Step 6: Sort by load (ascending), then priority (descending)
134
+ candidates.sort(key=lambda a: (
135
+ a.load,
136
+ -a.priority,
137
+ ))
138
+
139
+ # Step 7: Capacity check
140
+ candidates = [a for a in candidates if a.has_capacity]
141
+
142
+ return candidates[0] if candidates else None
143
+
144
+ async def assign(
145
+ self,
146
+ initiative: Any,
147
+ agent: Optional[Any] = None,
148
+ ) -> Optional[Any]:
149
+ """Assign initiative to agent.
150
+
151
+ Args:
152
+ initiative: Initiative to assign
153
+ agent: Specific agent to assign to (if None, auto-select)
154
+
155
+ Returns:
156
+ Assigned agent or None if assignment failed
157
+ """
158
+ # Get initiative ID and type
159
+ init_id = self._get_initiative_id(initiative)
160
+ if not init_id:
161
+ return None
162
+
163
+ # Select agent if not provided
164
+ if agent is None:
165
+ agent = self.select_agent(initiative)
166
+
167
+ if not agent:
168
+ logger.debug("No agent available for initiative %s", init_id)
169
+ return None
170
+
171
+ # Assign via store
172
+ assigned = self._initiative_store.assign(
173
+ init_id,
174
+ agent.agent_id,
175
+ agent_name=agent.name,
176
+ )
177
+
178
+ if assigned:
179
+ # Update agent assignment count
180
+ self._agent_store.increment_assignments(agent.agent_id)
181
+ logger.info(
182
+ "Assigned initiative %s to agent %s (%s)",
183
+ init_id,
184
+ agent.agent_id,
185
+ agent.name,
186
+ )
187
+ return agent
188
+
189
+ return None
190
+
191
+ async def auto_assign_pending(self, limit: int = 50) -> int:
192
+ """Auto-assign all pending initiatives.
193
+
194
+ Args:
195
+ limit: Maximum initiatives to assign
196
+
197
+ Returns:
198
+ Number of initiatives assigned
199
+ """
200
+ pending = self._initiative_store.list(
201
+ status=["pending"],
202
+ limit=limit,
203
+ )
204
+
205
+ assigned = 0
206
+ for initiative in pending:
207
+ agent = await self.assign(initiative)
208
+ if agent:
209
+ assigned += 1
210
+
211
+ return assigned
212
+
213
+ def _get_initiative_type(self, initiative: Any) -> str:
214
+ """Get initiative type from object or dict."""
215
+ if hasattr(initiative, "type"):
216
+ return initiative.type.value if hasattr(initiative.type, "value") else str(initiative.type)
217
+ if isinstance(initiative, dict):
218
+ return initiative.get("type", "unknown")
219
+ return "unknown"
220
+
221
+ def _get_initiative_id(self, initiative: Any) -> Optional[str]:
222
+ """Get initiative ID from object or dict."""
223
+ if hasattr(initiative, "id"):
224
+ return initiative.id
225
+ if isinstance(initiative, dict):
226
+ return initiative.get("id")
227
+ return None
228
+
229
+ def _get_preferred_agent_id(self, initiative: Any) -> Optional[str]:
230
+ """Get preferred agent ID from initiative."""
231
+ if hasattr(initiative, "preferred_agent_id"):
232
+ return initiative.preferred_agent_id
233
+ if isinstance(initiative, dict):
234
+ return initiative.get("preferred_agent_id")
235
+ return None
236
+
237
+ def _has_capacity(self, agent: Any) -> bool:
238
+ """Check if agent has capacity for more assignments."""
239
+ # Check max concurrent
240
+ if agent.current_assignments >= agent.max_concurrent:
241
+ return False
242
+
243
+ # Check hourly rate limit. The agents table carries a
244
+ # max_initiatives_per_hour column (default 10) that previously had no
245
+ # enforcement — counting `assigned` rows from the assignment_history
246
+ # within the last hour closes that gap. A value of 0 disables the
247
+ # limit; <0 is treated as 0 (off).
248
+ max_per_hour = getattr(agent, "max_initiatives_per_hour", 0) or 0
249
+ if max_per_hour > 0:
250
+ try:
251
+ since = datetime.now(timezone.utc) - timedelta(hours=1)
252
+ recent = self._initiative_store.count_agent_assignments_since(
253
+ agent.agent_id, since
254
+ )
255
+ if recent >= max_per_hour:
256
+ return False
257
+ except (AttributeError, TypeError):
258
+ # Store does not expose the counter (older deployments or
259
+ # unit-test doubles) — fall back to the max_concurrent gate
260
+ # rather than blocking all assignments.
261
+ pass
262
+
263
+ return True
@@ -0,0 +1,100 @@
1
+ """Read a deployment-selected backup attempt receipt, without running a backup.
2
+
3
+ The producer owns capture and publication. A receipt reports capture completion,
4
+ not archive verification, restore readiness, or coverage of other destinations.
5
+ """
6
+ from datetime import datetime, timezone
7
+ import hashlib
8
+ import json
9
+ from pathlib import Path
10
+ import re
11
+
12
+
13
+ MAX_RECEIPT_BYTES = 65536
14
+ MAX_AGE_DAYS = 7 # Preserve the existing operational backup review interval.
15
+
16
+
17
+ def _read(path):
18
+ with path.open('rb') as stream:
19
+ raw = stream.read(MAX_RECEIPT_BYTES + 1)
20
+ if len(raw) > MAX_RECEIPT_BYTES:
21
+ raise ValueError('receipt exceeds bound')
22
+ return raw, json.loads(raw)
23
+
24
+
25
+ def _time(value, now):
26
+ if not isinstance(value, str):
27
+ raise ValueError('missing timestamp')
28
+ parsed = datetime.fromisoformat(value.replace('Z', '+00:00'))
29
+ if parsed.tzinfo is None or parsed > now:
30
+ raise ValueError('timestamp is naive or in the future')
31
+ return parsed.astimezone(timezone.utc)
32
+
33
+
34
+ def backup_review_task(configured_path: str, now: datetime):
35
+ """Return a scoped review or None for a recent, bound successful receipt.
36
+
37
+ Contract v1: schema_version, status, completed_at, captured_at, receipt_path,
38
+ receipt_sha256. A captured attempt references an immutable local receipt by
39
+ relative path and SHA256; a failed attempt has null capture/reference fields.
40
+ Only the receipt bytes are verified here, never archive bytes or restoration.
41
+ """
42
+ path = Path(configured_path).expanduser().absolute()
43
+ task = {
44
+ 'entity_id': 'database_backup', 'entity_type': 'backup',
45
+ 'evidence_scope': 'configured_backup_receipt', 'evidence_path': str(path),
46
+ 'observed_at': now.isoformat(), 'receipt_status': 'unavailable',
47
+ 'captured_at': None, 'completed_at': None,
48
+ 'receipt_path': None, 'receipt_sha256': None,
49
+ 'description': 'Review configured backup evidence: latest attempt receipt is unavailable; capture freshness and recovery coverage are unknown',
50
+ }
51
+ try:
52
+ _, value = _read(path)
53
+ required = {'schema_version', 'status', 'completed_at', 'captured_at', 'receipt_path', 'receipt_sha256'}
54
+ if (not isinstance(value, dict) or not required.issubset(value)
55
+ or value.get('schema_version') != 1):
56
+ raise ValueError('unsupported receipt schema')
57
+ status = value.get('status')
58
+ completed = _time(value.get('completed_at'), now)
59
+ if status == 'captured':
60
+ captured = _time(value.get('captured_at'), now)
61
+ if captured > completed:
62
+ raise ValueError('capture follows completion')
63
+ relative = value.get('receipt_path')
64
+ digest = value.get('receipt_sha256')
65
+ if (not isinstance(relative, str) or not relative
66
+ or Path(relative).is_absolute()
67
+ or not isinstance(digest, str)
68
+ or not re.fullmatch('[0-9a-f]{64}', digest)):
69
+ raise ValueError('missing receipt reference')
70
+ receipt_path = (path.parent / relative).resolve()
71
+ if not receipt_path.is_relative_to(path.parent.resolve()):
72
+ raise ValueError('receipt is outside configured destination')
73
+ raw, receipt = _read(receipt_path)
74
+ if (hashlib.sha256(raw).hexdigest() != digest
75
+ or not isinstance(receipt, dict)
76
+ or receipt.get('status') != 'captured'
77
+ or _time(receipt.get('at'), now) != captured):
78
+ raise ValueError('capture receipt binding mismatch')
79
+ age = (now - captured).total_seconds() / 86400
80
+ if age <= MAX_AGE_DAYS:
81
+ return None
82
+ task.update(
83
+ captured_at=captured.isoformat(), receipt_path=str(receipt_path),
84
+ receipt_sha256=digest, age_days=age,
85
+ description=f'Review configured backup capture: latest completed capture is {age:.0f} days old; recovery readiness is unverified',
86
+ )
87
+ elif status == 'failed':
88
+ if any(value.get(key) is not None for key in ('captured_at', 'receipt_path', 'receipt_sha256')):
89
+ raise ValueError('failed attempt claims completed capture')
90
+ task['description'] = 'Review configured backup capture: latest attempt failed; earlier captures and recovery readiness require inspection'
91
+ else:
92
+ raise ValueError('unsupported attempt status')
93
+ task.update(receipt_status=status, completed_at=completed.isoformat())
94
+ except FileNotFoundError:
95
+ task['receipt_unavailable_reason'] = 'missing'
96
+ except OSError:
97
+ task['receipt_unavailable_reason'] = 'unreadable'
98
+ except (ValueError, TypeError, OverflowError):
99
+ task['receipt_unavailable_reason'] = 'invalid'
100
+ return task
@@ -0,0 +1,103 @@
1
+ """Context durability and freshness rules per initiative type (v0.16.0).
2
+
3
+ Context snapshots age differently per domain. ``days_since_contact`` is
4
+ still true hours later; ``"CI failing on main"`` or ``"meeting in 30
5
+ min"`` can become false while the initiative sits in the queue. Each
6
+ initiative type therefore declares its context durability:
7
+
8
+ - ``durable`` — snapshot at creation, safe to persist and read later.
9
+ - ``volatile`` — must be refreshed at read time, or checked against a
10
+ freshness TTL before the agent acts on it.
11
+
12
+ The agent-side contract: a volatile initiative's context carries
13
+ ``context_captured_at``. Observation-backed proposals retain the actual source
14
+ observation time rather than renewing it when a stored row is read. Before acting,
15
+ the agent checks ``is_context_fresh()``; if stale, it calls
16
+ ``POST /v1/host/initiatives/{id}/context/refresh`` which routes to the
17
+ engine's per-entity ``rebuild_context()``.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from datetime import datetime, timezone
23
+ from typing import Dict, Optional
24
+
25
+ DURABLE = "durable"
26
+ VOLATILE = "volatile"
27
+
28
+ # Durability by InitiativeType value. Unlisted types default to durable
29
+ # with a conservative short TTL applied by callers that care.
30
+ CONTEXT_DURABILITY: Dict[str, str] = {
31
+ # Existing types
32
+ "follow_up": DURABLE,
33
+ "relationship": DURABLE,
34
+ "introduction": DURABLE,
35
+ "health": VOLATILE,
36
+ "scheduling": DURABLE,
37
+ "coding": VOLATILE,
38
+ "subsystem_health": VOLATILE,
39
+ "data_quality": VOLATILE,
40
+ "operational": DURABLE,
41
+ "capability_gap": DURABLE,
42
+ "knowledge_acquisition": DURABLE,
43
+ "behavioral_correction": DURABLE,
44
+ "agent_action": VOLATILE,
45
+ # Autonomous work domains (v0.16.0)
46
+ "commitment": DURABLE,
47
+ "calendar": VOLATILE,
48
+ "research": DURABLE,
49
+ "task": DURABLE,
50
+ "project": DURABLE,
51
+ "system": VOLATILE,
52
+ }
53
+
54
+ # Freshness TTL (seconds) for volatile types: how old a context snapshot
55
+ # may be before the agent must refresh it.
56
+ CONTEXT_FRESHNESS_TTL_SECONDS: Dict[str, int] = {
57
+ "calendar": 300,
58
+ "coding": 600,
59
+ "system": 300,
60
+ "health": 900,
61
+ "subsystem_health": 600,
62
+ "data_quality": 3600,
63
+ "agent_action": 600,
64
+ }
65
+
66
+ _DEFAULT_VOLATILE_TTL = 600
67
+
68
+
69
+ def durability_for(type_value: str) -> str:
70
+ """Declared durability for an initiative type ('durable'/'volatile')."""
71
+ return CONTEXT_DURABILITY.get(type_value, DURABLE)
72
+
73
+
74
+ def freshness_ttl_for(type_value: str) -> Optional[int]:
75
+ """TTL in seconds for volatile types; None for durable types."""
76
+ if durability_for(type_value) == DURABLE:
77
+ return None
78
+ return CONTEXT_FRESHNESS_TTL_SECONDS.get(type_value, _DEFAULT_VOLATILE_TTL)
79
+
80
+
81
+ def is_context_fresh(
82
+ type_value: str,
83
+ captured_at: Optional[str],
84
+ now: Optional[datetime] = None,
85
+ ) -> bool:
86
+ """Whether a context snapshot is still safe to act on.
87
+
88
+ Durable contexts are always fresh. Volatile contexts without a
89
+ capture timestamp, or with a future timestamp, are treated as stale.
90
+ """
91
+ ttl = freshness_ttl_for(type_value)
92
+ if ttl is None:
93
+ return True
94
+ if not captured_at:
95
+ return False
96
+ try:
97
+ stamp = datetime.fromisoformat(captured_at.replace("Z", "+00:00"))
98
+ except (ValueError, TypeError):
99
+ return False
100
+ if stamp.tzinfo is None:
101
+ stamp = stamp.replace(tzinfo=timezone.utc)
102
+ now = now or datetime.now(timezone.utc)
103
+ return 0 <= (now - stamp).total_seconds() <= ttl