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,268 @@
1
+ """DeliveryRateLimiter — per-person rate limiting for proactive messages.
2
+
3
+ Rules:
4
+ - Max 3 proactive messages per day per person
5
+ - No proactive messages between 22:00–08:00 local time (unless critical)
6
+ - At least 2 hours between proactive messages to the same person
7
+ - User can disable proactive messaging entirely (checked externally)
8
+
9
+ Counts can optionally be persisted to a small SQLite file so restarts don't
10
+ let a crashloop bypass the daily cap. Persistence is opt-in via ``db_path``;
11
+ when ``db_path`` is None the limiter stays purely in-memory (used in tests).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from contextlib import closing
17
+ import logging
18
+ import sqlite3
19
+ from collections import defaultdict
20
+ from datetime import datetime, timedelta, timezone
21
+ from pathlib import Path
22
+ from typing import Any, Dict, List, Optional
23
+
24
+ from apsimo.util.quiet_hours import in_quiet_window
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ _MAX_PER_DAY = 3
29
+ _COOLDOWN_HOURS = 2
30
+ _QUIET_START_HOUR = 22
31
+ _QUIET_END_HOUR = 8
32
+
33
+
34
+ def _resolve_tz():
35
+ """Owner-local timezone for quiet hours: COLONY_TIMEZONE, else system, else UTC."""
36
+ import os
37
+ name = os.environ.get("COLONY_TIMEZONE", "").strip()
38
+ if name:
39
+ try:
40
+ from zoneinfo import ZoneInfo
41
+ return ZoneInfo(name)
42
+ except Exception:
43
+ logger.warning("rate_limiter: invalid COLONY_TIMEZONE %r, using system tz", name)
44
+ try:
45
+ local = datetime.now().astimezone().tzinfo
46
+ if local is not None:
47
+ return local
48
+ except Exception:
49
+ pass
50
+ return timezone.utc
51
+ # Deliveries older than this many days are pruned at startup; nothing in the
52
+ # rate-limit logic needs more than ~48h of history.
53
+ _HISTORY_RETENTION_DAYS = 3
54
+
55
+
56
+ class DeliveryRateLimiter:
57
+ """Per-person rate limiter for proactive message delivery.
58
+
59
+ Tracks delivery timestamps in memory for fast ``can_deliver`` checks, and
60
+ (optionally) mirrors them to a SQLite log so counts survive restarts.
61
+ """
62
+
63
+ def __init__(
64
+ self,
65
+ max_per_day: int = _MAX_PER_DAY,
66
+ cooldown_hours: int = _COOLDOWN_HOURS,
67
+ quiet_start_hour: int = _QUIET_START_HOUR,
68
+ quiet_end_hour: int = _QUIET_END_HOUR,
69
+ metrics: Optional[Any] = None,
70
+ db_path: Optional[Path] = None,
71
+ cap_provider: Optional[Any] = None,
72
+ ) -> None:
73
+ self._max_per_day = max_per_day
74
+ # Adaptive cap (Amendment 1.6): an optional callable(base:int)->int
75
+ # (the trust engine) that EARNS the daily cap upward with a proven
76
+ # delivery track record. Bounded by the provider; base is the floor
77
+ # semantics owners already know. Failure falls back to base.
78
+ self._cap_provider = cap_provider
79
+ self._cooldown = timedelta(hours=cooldown_hours)
80
+ self._quiet_start = quiet_start_hour
81
+ self._quiet_end = quiet_end_hour
82
+ self._metrics = metrics
83
+ # Quiet hours + the daily bucket are evaluated in the OWNER's local
84
+ # timezone (22:00-08:00 must mean their night, not a UTC window).
85
+ self._tz = _resolve_tz()
86
+
87
+ # person_id → list of delivery timestamps (UTC) today
88
+ self._daily_counts: Dict[str, List[datetime]] = defaultdict(list)
89
+ # person_id → last delivery timestamp (UTC)
90
+ self._last_delivery: Dict[str, Optional[datetime]] = defaultdict(lambda: None)
91
+ self._today = datetime.now(self._tz).date()
92
+
93
+ # Frustration back-off: person_id → probability (updated by autonomy loop)
94
+ self._frustration_cache: Dict[str, float] = {}
95
+
96
+ # Optional persistence. Failing to init the DB must not take the
97
+ # limiter offline — fall back to pure in-memory with a warning.
98
+ self._db_path: Optional[Path] = Path(db_path) if db_path else None
99
+ if self._db_path is not None:
100
+ try:
101
+ self._init_db()
102
+ self._reload_from_db()
103
+ except Exception as exc:
104
+ logger.warning(
105
+ "rate_limiter: persistence disabled — %s: %s",
106
+ type(exc).__name__, exc,
107
+ )
108
+ self._db_path = None
109
+
110
+ def can_deliver(self, person_id: str, urgency: float = 0.5) -> tuple[bool, str]:
111
+ """Check if a proactive message can be delivered to person_id.
112
+
113
+ Args:
114
+ person_id: Recipient identifier
115
+ urgency: Message urgency (0-1). Messages with urgency >= 0.9 bypass quiet hours.
116
+
117
+ Returns:
118
+ (allowed: bool, reason: str)
119
+ """
120
+ self._reset_if_new_day()
121
+
122
+ # Quiet hours check (bypassed for critical messages)
123
+ if urgency < 0.9 and self._in_quiet_hours():
124
+ return False, "quiet_hours"
125
+
126
+ # Frustration back-off (bypassed for critical messages)
127
+ if urgency < 0.9:
128
+ frust = self._frustration_cache.get(person_id, 0.0)
129
+ if frust > 0.5:
130
+ if self._metrics is not None:
131
+ try:
132
+ self._metrics.record_sentiment_suppression()
133
+ except Exception:
134
+ pass
135
+ return False, f"person_frustrated (p={frust:.0%})"
136
+
137
+ # Daily limit (adaptive when a trust cap provider is wired)
138
+ effective_max = self._max_per_day
139
+ if self._cap_provider is not None:
140
+ try:
141
+ effective_max = max(1, int(self._cap_provider(self._max_per_day)))
142
+ except Exception:
143
+ effective_max = self._max_per_day
144
+ count = len(self._daily_counts[person_id])
145
+ if count >= effective_max:
146
+ return False, f"daily_limit_reached ({count}/{effective_max})"
147
+
148
+ # Cooldown
149
+ last = self._last_delivery[person_id]
150
+ if last is not None:
151
+ elapsed = datetime.now(timezone.utc) - last
152
+ if elapsed < self._cooldown:
153
+ remaining = int((self._cooldown - elapsed).total_seconds()) // 60
154
+ return False, f"cooldown_active ({remaining}m remaining)"
155
+
156
+ return True, "ok"
157
+
158
+ def record_delivery(self, person_id: str) -> None:
159
+ """Mark that a proactive message was successfully delivered."""
160
+ self._reset_if_new_day()
161
+ now = datetime.now(timezone.utc)
162
+ self._daily_counts[person_id].append(now)
163
+ self._last_delivery[person_id] = now
164
+ if self._db_path is not None:
165
+ try:
166
+ with closing(sqlite3.connect(self._db_path)) as conn, conn:
167
+ conn.execute(
168
+ "INSERT INTO delivery_log (person_id, delivered_at) VALUES (?, ?)",
169
+ (person_id, now.isoformat()),
170
+ )
171
+ except Exception as exc:
172
+ logger.warning(
173
+ "rate_limiter: failed to persist delivery for %s: %s",
174
+ person_id, exc,
175
+ )
176
+ logger.debug(
177
+ "Delivery recorded for %s (count today: %d)",
178
+ person_id,
179
+ len(self._daily_counts[person_id]),
180
+ )
181
+
182
+ def update_frustration(self, person_id: str, probability: float) -> None:
183
+ """Update cached frustration probability for a person.
184
+
185
+ Called by the autonomy loop after running the state estimator.
186
+ Values above 0.5 will suppress non-critical deliveries.
187
+ """
188
+ self._frustration_cache[person_id] = probability
189
+
190
+ def daily_count(self, person_id: str) -> int:
191
+ """Return number of proactive deliveries today for a person."""
192
+ self._reset_if_new_day()
193
+ return len(self._daily_counts[person_id])
194
+
195
+ # ------------------------------------------------------------------
196
+ # Helpers
197
+ # ------------------------------------------------------------------
198
+
199
+ def _reset_if_new_day(self) -> None:
200
+ """Reset daily counters when the UTC date rolls over."""
201
+ today = datetime.now(self._tz).date()
202
+ if today != self._today:
203
+ self._daily_counts.clear()
204
+ self._today = today
205
+
206
+ def _in_quiet_hours(self) -> bool:
207
+ """Return True if current time is within quiet hours (owner-local tz)."""
208
+ now = datetime.now(self._tz)
209
+ return in_quiet_window(
210
+ now.hour * 60 + now.minute,
211
+ self._quiet_start * 60,
212
+ self._quiet_end * 60,
213
+ )
214
+
215
+ # ------------------------------------------------------------------
216
+ # Persistence
217
+ # ------------------------------------------------------------------
218
+
219
+ def _init_db(self) -> None:
220
+ """Create the delivery-log table on first use and prune stale rows."""
221
+ assert self._db_path is not None
222
+ self._db_path.parent.mkdir(parents=True, exist_ok=True)
223
+ with closing(sqlite3.connect(self._db_path)) as conn, conn:
224
+ conn.execute(
225
+ "CREATE TABLE IF NOT EXISTS delivery_log ("
226
+ " person_id TEXT NOT NULL,"
227
+ " delivered_at TEXT NOT NULL"
228
+ ")"
229
+ )
230
+ conn.execute(
231
+ "CREATE INDEX IF NOT EXISTS idx_delivery_person_date "
232
+ "ON delivery_log(person_id, delivered_at)"
233
+ )
234
+ cutoff = (
235
+ datetime.now(timezone.utc) - timedelta(days=_HISTORY_RETENTION_DAYS)
236
+ ).isoformat()
237
+ conn.execute(
238
+ "DELETE FROM delivery_log WHERE delivered_at < ?", (cutoff,),
239
+ )
240
+
241
+ def _reload_from_db(self) -> None:
242
+ """Reload today's counts and the most-recent delivery per person."""
243
+ assert self._db_path is not None
244
+ # "Today" is the OWNER-LOCAL date (matching _reset_if_new_day), so
245
+ # its start is local midnight converted to UTC — the log stores UTC
246
+ # timestamps. Combining the local date with UTC midnight shifted the
247
+ # reload window by the UTC offset, over- or under-counting restarts.
248
+ today_start = datetime.combine(
249
+ self._today, datetime.min.time(), tzinfo=self._tz
250
+ ).astimezone(timezone.utc).isoformat()
251
+ with closing(sqlite3.connect(self._db_path)) as conn, conn:
252
+ # Today's deliveries → daily count
253
+ cur = conn.execute(
254
+ "SELECT person_id, delivered_at FROM delivery_log "
255
+ "WHERE delivered_at >= ? ORDER BY delivered_at",
256
+ (today_start,),
257
+ )
258
+ for person_id, ts_str in cur:
259
+ self._daily_counts[person_id].append(datetime.fromisoformat(ts_str))
260
+
261
+ # Most recent delivery per person → cooldown state (spans day boundary)
262
+ cur = conn.execute(
263
+ "SELECT person_id, MAX(delivered_at) FROM delivery_log "
264
+ "GROUP BY person_id"
265
+ )
266
+ for person_id, ts_str in cur:
267
+ if ts_str:
268
+ self._last_delivery[person_id] = datetime.fromisoformat(ts_str)
@@ -0,0 +1,206 @@
1
+ """Reach-out delivery policy: sanitisation, staleness, quiet-hours urgency.
2
+
3
+ These are the quality/safety guards applied to a user-facing reach-out
4
+ initiative *before* it is handed to the guarded Hermes delivery path. They are
5
+ generic and deployment-agnostic (patterns + env-tunable thresholds, no
6
+ hardcoded identities or messages).
7
+
8
+ * sanitise -- strip raw conversation / skill / control markup from the
9
+ outward-facing text so Hermes composes from clean intent.
10
+ * staleness -- drop reach-outs whose subject is older than a max age; a late
11
+ autonomous ping about a two-week-stale item is noise.
12
+ * quiet -- reach-out must respect quiet hours by default; a high
13
+ stale-age priority must NOT silently bypass the quiet window.
14
+ Only an explicit urgent signal may exceed the cap.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import os
20
+ import re
21
+ from datetime import datetime, timezone
22
+ from typing import Any, Optional
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # Sanitisation
26
+ # ---------------------------------------------------------------------------
27
+
28
+ # ``<<RCSCTX ...>>`` and any other ``<<...>>`` control block, terminated or not
29
+ # (titles are truncated so the closing ``>>`` is frequently missing).
30
+ _ANGLE_BLOCK = re.compile(r"<<.*?>>", re.DOTALL)
31
+ _ANGLE_UNTERMINATED = re.compile(r"<<[^>]*$", re.DOTALL)
32
+
33
+ # Bracketed system/skill directives, e.g.
34
+ # [IMPORTANT: The user has invoked the "colony-operations" skill ...]
35
+ # [System note: Your previous turn was interrupted ...]
36
+ _BRACKET_DIRECTIVE = re.compile(
37
+ r"\[\s*(?:IMPORTANT|SYSTEM\s*NOTE|SYSTEM|INTERNAL|CONTEXT|NOTE)\b[^\]]*\]",
38
+ re.IGNORECASE | re.DOTALL,
39
+ )
40
+ _BRACKET_UNTERMINATED = re.compile(
41
+ r"\[\s*(?:IMPORTANT|SYSTEM\s*NOTE|SYSTEM|INTERNAL|CONTEXT|NOTE)\b[^\]]*$",
42
+ re.IGNORECASE | re.DOTALL,
43
+ )
44
+
45
+ # Non-printable / control characters (keep tab/newline out of outward text too).
46
+ _CONTROL = re.compile(r"[\x00-\x1f\x7f]")
47
+ _WS = re.compile(r"\s+")
48
+
49
+ _SANITIZE_FIELDS = ("title", "description", "rationale", "suggested_action")
50
+
51
+
52
+ def sanitize_text(text: Optional[str]) -> str:
53
+ """Strip control/conversation/skill markup and collapse whitespace."""
54
+ if not text:
55
+ return ""
56
+ s = str(text)
57
+ s = _ANGLE_BLOCK.sub(" ", s)
58
+ s = _ANGLE_UNTERMINATED.sub(" ", s)
59
+ s = _BRACKET_DIRECTIVE.sub(" ", s)
60
+ s = _BRACKET_UNTERMINATED.sub(" ", s)
61
+ s = _CONTROL.sub(" ", s)
62
+ s = _WS.sub(" ", s).strip()
63
+ return s
64
+
65
+
66
+ def sanitize_payload(payload: dict) -> dict:
67
+ """Return a copy of the initiative payload with its text fields cleaned."""
68
+ out = dict(payload)
69
+ for field in _SANITIZE_FIELDS:
70
+ value = out.get(field)
71
+ if isinstance(value, str):
72
+ out[field] = sanitize_text(value)
73
+ if not out.get("title"):
74
+ out["title"] = (out.get("description") or "")[:80] or "(follow-up)"
75
+ return out
76
+
77
+
78
+ # ---------------------------------------------------------------------------
79
+ # Meaningful-content gate (applied at reach-out GENERATION time)
80
+ # ---------------------------------------------------------------------------
81
+
82
+ # Minimum real alphanumeric substance a reach-out source must retain after
83
+ # sanitisation to be worth generating an initiative from. Low enough to keep
84
+ # legitimately short subjects ("Call Bob"), high enough to drop fragments.
85
+ _MIN_MEANINGFUL_CHARS = 4
86
+
87
+ # Markers that a source turn is system / skill / non-conversational, i.e. not
88
+ # genuine conversation or a real commitment. These identify origins that
89
+ # sanitisation alone might leave partial residue from.
90
+ _SYSTEM_ORIGIN = re.compile(
91
+ r"\binvoked\b.{0,60}?\bskill\b" # "... invoked the <name> skill ..."
92
+ r"|previous turn was interrupted"
93
+ r"|\bsystem note\b"
94
+ r"|\bsystem[- ]?generated\b"
95
+ r"|conversation (?:was )?(?:interrupted|truncated|reset)"
96
+ r"|<\s*/?\s*system[\s>]", # <system> ... </system> style tags
97
+ re.IGNORECASE | re.DOTALL,
98
+ )
99
+
100
+
101
+ def is_system_origin(text: Optional[str]) -> bool:
102
+ """True if the raw source text is a system/skill/non-conversational turn."""
103
+ return bool(text) and bool(_SYSTEM_ORIGIN.search(str(text)))
104
+
105
+
106
+ def meaningful_reachout_text(text: Optional[str]) -> str:
107
+ """Clean reach-out source text, or "" if it is not worth surfacing.
108
+
109
+ Returns the sanitised text when the source is genuine, human-meaningful
110
+ content; returns "" when the source is empty, of system/skill origin, or
111
+ has no real substance after sanitisation. Use at reach-out GENERATION time
112
+ so near-empty / system-origin junk never becomes an initiative.
113
+ """
114
+ if not text or is_system_origin(text):
115
+ return ""
116
+ cleaned = sanitize_text(text)
117
+ substance = re.sub(r"[^0-9a-z]+", "", cleaned.lower())
118
+ if len(substance) < _MIN_MEANINGFUL_CHARS:
119
+ return ""
120
+ return cleaned
121
+
122
+
123
+ # ---------------------------------------------------------------------------
124
+ # Staleness
125
+ # ---------------------------------------------------------------------------
126
+
127
+ # Structured "how overdue is the subject" signals. Deliberately excludes
128
+ # contact-recency signals (e.g. days_since_contact) which are a *reason* to
129
+ # reach out, not a disqualifier.
130
+ _STALENESS_KEYS = (
131
+ "days_pending", "days_overdue", "days_stale", "stale_days", "age_days",
132
+ )
133
+
134
+
135
+ def _scan_staleness(obj: Any, depth: int = 2) -> float:
136
+ best = 0.0
137
+ if isinstance(obj, dict):
138
+ for key, value in obj.items():
139
+ if key in _STALENESS_KEYS and isinstance(value, (int, float)):
140
+ best = max(best, float(value))
141
+ elif depth > 0 and isinstance(value, (dict, list)):
142
+ best = max(best, _scan_staleness(value, depth - 1))
143
+ elif isinstance(obj, list) and depth > 0:
144
+ for value in obj:
145
+ best = max(best, _scan_staleness(value, depth - 1))
146
+ return best
147
+
148
+
149
+ def reachout_age_days(payload: dict) -> float:
150
+ """Effective age of a reach-out: max of structured subject-staleness and
151
+ the initiative's own age since generation."""
152
+ age = _scan_staleness(payload.get("context") or {})
153
+ generated_at = payload.get("generated_at")
154
+ if generated_at:
155
+ try:
156
+ ts = datetime.fromisoformat(str(generated_at).replace("Z", "+00:00"))
157
+ if ts.tzinfo is None:
158
+ ts = ts.replace(tzinfo=timezone.utc)
159
+ init_age = (datetime.now(timezone.utc) - ts).total_seconds() / 86400.0
160
+ age = max(age, init_age)
161
+ except Exception:
162
+ pass
163
+ return age
164
+
165
+
166
+ def max_age_days() -> float:
167
+ try:
168
+ return float(os.environ.get("COLONY_REACHOUT_MAX_AGE_DAYS", "7"))
169
+ except (TypeError, ValueError):
170
+ return 7.0
171
+
172
+
173
+ def is_aged_out(payload: dict) -> bool:
174
+ """True if this reach-out is too old to be worth surfacing."""
175
+ return reachout_age_days(payload) > max_age_days()
176
+
177
+
178
+ # ---------------------------------------------------------------------------
179
+ # Quiet-hours urgency
180
+ # ---------------------------------------------------------------------------
181
+
182
+ # The rate limiter bypasses quiet hours at urgency >= 0.9. Reach-out urgency is
183
+ # derived from priority, which for stale items pins to 1.0 -- an artifact, not a
184
+ # genuine "wake them up" signal. Cap it below the bypass threshold by default.
185
+ def _urgency_cap() -> float:
186
+ try:
187
+ return float(os.environ.get("COLONY_REACHOUT_URGENCY_CAP", "0.85"))
188
+ except (TypeError, ValueError):
189
+ return 0.85
190
+
191
+
192
+ def _explicit_urgent(payload: dict) -> bool:
193
+ if payload.get("urgent") is True or payload.get("bypass_quiet_hours") is True:
194
+ return True
195
+ ctx = payload.get("context") or {}
196
+ if isinstance(ctx, dict):
197
+ return ctx.get("urgent") is True or ctx.get("bypass_quiet_hours") is True
198
+ return False
199
+
200
+
201
+ def quiet_hours_urgency(payload: dict, raw_urgency: float) -> float:
202
+ """Urgency to use for the delivery gate. Reach-out respects quiet hours
203
+ unless an explicit urgent flag is set on the initiative/context."""
204
+ if _explicit_urgent(payload):
205
+ return raw_urgency
206
+ return min(raw_urgency, _urgency_cap())
@@ -0,0 +1,22 @@
1
+ """Directed action: owner directive -> gated, delegated, audited execution.
2
+
3
+ Option A architecture: Colony never mutates external systems itself. It
4
+ scopes the directive deterministically, gates it (boundaries first, then
5
+ approval), dispatches the contract to an env-configured delegate, audits
6
+ what actually happened against the granted scope, and reports back through
7
+ the guarded reach-out path.
8
+ """
9
+
10
+ from apsimo.directed.models import (
11
+ ScopedTask, ScopedTaskStore, ScopeLimits, READ_OPS, MUTATE_OPS,
12
+ )
13
+ from apsimo.directed.intake import scope_from_directive, resolve_targets
14
+ from apsimo.directed.audit import audit_completion, audit_via_report
15
+ from apsimo.directed.service import DirectedActionService, directed_mode
16
+
17
+ __all__ = [
18
+ "ScopedTask", "ScopedTaskStore", "ScopeLimits", "READ_OPS", "MUTATE_OPS",
19
+ "scope_from_directive", "resolve_targets",
20
+ "audit_completion", "audit_via_report",
21
+ "DirectedActionService", "directed_mode",
22
+ ]
@@ -0,0 +1,167 @@
1
+ """Post-action verification: what actually happened vs the granted scope.
2
+
3
+ Two evidence sources, best-first:
4
+ 1. Repo inspection (when a read-only local mirror of the target exists):
5
+ fetch the work branch and diff it against its merge-base -- files touched,
6
+ commit count, branch name are verified MECHANICALLY.
7
+ 2. The delegate's structured report, cross-checked field by field.
8
+
9
+ Any out-of-scope observation is a VIOLATION: flagged loudly, recorded on the
10
+ task, and fed to the outcome feedback store by the service.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import fnmatch
16
+ import logging
17
+ import subprocess
18
+ from typing import Any, Dict, List, Optional
19
+
20
+ from apsimo.directed.models import ScopedTask, MUTATE_OPS
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ def _match_globs(path: str, globs: List[str]) -> bool:
26
+ p = (path or "").lstrip("./")
27
+ for g in globs or ["**"]:
28
+ if g in ("**", "*") or fnmatch.fnmatch(p, g) or fnmatch.fnmatch(p, g.rstrip("/") + "/**"):
29
+ return True
30
+ return False
31
+
32
+
33
+ def _git(mirror_path: str, *args: str, timeout: int = 30) -> str:
34
+ out = subprocess.run(
35
+ ["git", "-C", mirror_path, *args],
36
+ capture_output=True, text=True, timeout=timeout,
37
+ )
38
+ if out.returncode != 0:
39
+ raise RuntimeError(out.stderr.strip()[:200])
40
+ return out.stdout
41
+
42
+
43
+ def audit_via_mirror(task: ScopedTask, mirror_path: str, branch: str,
44
+ base: str = "HEAD") -> Dict[str, Any]:
45
+ """Mechanical audit from a read-only mirror: fetch the branch, diff vs base."""
46
+ findings: List[str] = []
47
+ ok = True
48
+ try:
49
+ _git(mirror_path, "fetch", "origin", branch, timeout=60)
50
+ merge_base = _git(mirror_path, "merge-base", base, "FETCH_HEAD").strip()
51
+ commits = _git(mirror_path, "rev-list", "--count", f"{merge_base}..FETCH_HEAD").strip()
52
+ n_commits = int(commits or 0)
53
+ files = [f for f in _git(
54
+ mirror_path, "diff", "--name-only", merge_base, "FETCH_HEAD",
55
+ ).splitlines() if f.strip()]
56
+ except Exception as exc:
57
+ return {"method": "mirror", "ok": None,
58
+ "findings": [f"mirror inspection unavailable: {exc}"]}
59
+
60
+ if not branch.startswith(task.limits.branch_prefix):
61
+ ok = False
62
+ findings.append(f"branch {branch!r} outside required prefix {task.limits.branch_prefix!r}")
63
+ if n_commits > task.limits.max_commits:
64
+ ok = False
65
+ findings.append(f"{n_commits} commits exceeds cap {task.limits.max_commits}")
66
+ off = [f for f in files if not _match_globs(f, task.limits.path_globs)]
67
+ if off:
68
+ ok = False
69
+ findings.append(f"files outside allowed paths: {off[:10]}")
70
+ return {"method": "mirror", "ok": ok, "branch": branch,
71
+ "commits": n_commits, "files_touched": files[:100], "findings": findings}
72
+
73
+
74
+ def audit_via_report(task: ScopedTask, report: Dict[str, Any]) -> Dict[str, Any]:
75
+ """Cross-check the delegate's structured report against the scope."""
76
+ findings: List[str] = []
77
+ ok = True
78
+ report = report or {}
79
+
80
+ ops = [str(o).lower() for o in (report.get("operations") or [])]
81
+ illegal = [o for o in ops if o not in set(task.allowed_ops)]
82
+ if illegal:
83
+ ok = False
84
+ findings.append(f"operations outside scope: {illegal}")
85
+
86
+ branch = str(report.get("branch") or "")
87
+ did_mutate = bool(set(ops) & MUTATE_OPS) or bool(report.get("commits"))
88
+ if did_mutate and not task.mutating:
89
+ ok = False
90
+ findings.append("delegate mutated on a read-only scope")
91
+ if did_mutate and branch and not branch.startswith(task.limits.branch_prefix):
92
+ ok = False
93
+ findings.append(f"branch {branch!r} outside required prefix {task.limits.branch_prefix!r}")
94
+
95
+ n_commits = int(report.get("commits") or 0)
96
+ if n_commits > task.limits.max_commits:
97
+ ok = False
98
+ findings.append(f"{n_commits} commits exceeds cap {task.limits.max_commits}")
99
+
100
+ if report.get("force_push"):
101
+ ok = False
102
+ findings.append("force push reported (never allowed)")
103
+ if report.get("deletions") and not task.limits.delete_allowed:
104
+ deld = report.get("deletions")
105
+ off = [f for f in deld if not _match_globs(str(f), task.limits.path_globs)] if isinstance(deld, list) else [str(deld)]
106
+ if off:
107
+ ok = False
108
+ findings.append(f"deletes outside scope: {off[:10]}")
109
+
110
+ files = [str(f) for f in (report.get("files_touched") or [])]
111
+ off = [f for f in files if not _match_globs(f, task.limits.path_globs)]
112
+ if off:
113
+ ok = False
114
+ findings.append(f"files outside allowed paths: {off[:10]}")
115
+
116
+ missing = [k for k in task.reporting if k not in report]
117
+ if missing:
118
+ findings.append(f"report missing expected fields: {missing}")
119
+ if ok:
120
+ # What was not reported cannot be verified: an empty/partial
121
+ # report is "unverified" (ok=None), never "clean" (ok=True).
122
+ ok = None
123
+
124
+ return {"method": "report", "ok": ok, "branch": branch, "commits": n_commits,
125
+ "files_touched": files[:100], "findings": findings}
126
+
127
+
128
+ def audit_completion(task: ScopedTask, report: Dict[str, Any],
129
+ mirror_path: Optional[str] = None) -> Dict[str, Any]:
130
+ """Full audit: mirror inspection when reachable, report cross-check always.
131
+
132
+ Verdict: 'clean' | 'violation' | 'unverified'. A violation from EITHER
133
+ source wins (fail loud).
134
+ """
135
+ report_audit = audit_via_report(task, report)
136
+ mirror_audit = None
137
+ branch = report_audit.get("branch") or ""
138
+ if mirror_path and branch:
139
+ mirror_audit = audit_via_mirror(task, mirror_path, branch)
140
+ elif mirror_path and not branch and task.mutating:
141
+ # A mirror was available but the mutating delegate reported no
142
+ # branch, so the mirror inspection silently could not run: the
143
+ # report side may not verdict "clean" on its own say-so.
144
+ report_audit["findings"].append(
145
+ "mirror audit skipped: no branch reported for mutating task")
146
+ if report_audit.get("ok") is True:
147
+ report_audit["ok"] = None
148
+
149
+ oks = [a["ok"] for a in (report_audit, mirror_audit) if a is not None]
150
+ if any(o is False for o in oks):
151
+ verdict = "violation"
152
+ elif any(o is True for o in oks):
153
+ verdict = "clean"
154
+ else:
155
+ verdict = "unverified"
156
+
157
+ result = {
158
+ "verdict": verdict,
159
+ "report_audit": report_audit,
160
+ "mirror_audit": mirror_audit,
161
+ }
162
+ if verdict == "violation":
163
+ all_findings = report_audit.get("findings", []) + (
164
+ (mirror_audit or {}).get("findings", []) if mirror_audit else [])
165
+ logger.warning("DIRECTED-ACTION SCOPE VIOLATION on %s: %s",
166
+ task.id, "; ".join(all_findings)[:400])
167
+ return result