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,509 @@
1
+ """DirectedActionService -- owner directive -> gated, delegated, audited action.
2
+
3
+ Pipeline (option A: delegation only, Colony never mutates anything itself):
4
+
5
+ intake -> deterministic ScopedTask (intake.py)
6
+ gate 1 -> DirectiveGuard boundary check FIRST (a standing "leave X alone"
7
+ refuses intake with the citation)
8
+ gate 2 -> approval tiering: read-only scopes auto-approve; any mutating
9
+ scope requires owner approval (legacy repeat approvals are
10
+ expiring and use-capped during migration)
11
+ dispatch-> POST the ScopedTask contract to the EXISTING env-configured
12
+ delegate endpoint (COLONY_DIRECTED_TASK_URL, falling back to the
13
+ agent-bridge jobs webhook). Dry-run mode logs the exact would-be
14
+ dispatch and sends nothing (COLONY_DIRECTED_MODE, default dry_run).
15
+ report -> the delegate POSTs a structured report back; audit_completion
16
+ verifies it against the granted scope (mirror inspection when a
17
+ read-only mirror of the target exists).
18
+ outcome -> violations flagged loudly + recorded; success/violation/failure
19
+ feeds the TypeFeedbackStore; an owner-facing report artifact is
20
+ routed through the guarded reach-out path (held by delivery shadow).
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import hashlib
26
+ import hmac
27
+ import logging
28
+ import os
29
+ import time
30
+ import uuid
31
+ from typing import Any, Dict, List, Optional
32
+
33
+ from apsimo.directed.models import ScopedTask, ScopedTaskStore
34
+ from apsimo.directed.intake import scope_from_directive
35
+ from apsimo.directed.audit import audit_completion
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+
40
+ def directed_mode() -> str:
41
+ from apsimo.util.autonomy_preset import resolve
42
+ return resolve("COLONY_DIRECTED_MODE", ("off", "dry_run", "live"),
43
+ "dry_run")
44
+
45
+
46
+ def _dispatch_url() -> str:
47
+ return (os.environ.get("COLONY_DIRECTED_TASK_URL", "")
48
+ or os.environ.get("COLONY_BRIDGE_JOBS_WEBHOOK_URL", "")
49
+ or os.environ.get("COLONY_JOBS_WEBHOOK_URL", ""))
50
+
51
+
52
+ def _strict_reports() -> bool:
53
+ """Strict report-back (default): a completion report is accepted only for
54
+ a task that was actually dispatched. COLONY_DIRECTED_STRICT_REPORTS=0
55
+ restores the legacy accept-in-any-status behavior."""
56
+ return os.environ.get("COLONY_DIRECTED_STRICT_REPORTS", "1").strip() != "0"
57
+
58
+
59
+ def _max_dispatch_per_day() -> int:
60
+ """Daily dispatch cap (rolling 24h). <=0 disables the cap (legacy)."""
61
+ try:
62
+ return int(os.environ.get("COLONY_DIRECTED_MAX_DISPATCH_PER_DAY", "10"))
63
+ except (TypeError, ValueError):
64
+ return 10
65
+
66
+
67
+ def _hmac_key() -> bytes:
68
+ """Optional shared secret for the dispatch envelope. Empty = unsigned."""
69
+ return os.environ.get("COLONY_DIRECTED_HMAC_KEY", "").encode("utf-8")
70
+
71
+
72
+ def _sign(key: bytes, message: str) -> str:
73
+ return hmac.new(key, message.encode("utf-8"), hashlib.sha256).hexdigest()
74
+
75
+
76
+ def report_token_for(task_id: str) -> str:
77
+ """Deterministic report-back token for a task (only when a key is set).
78
+
79
+ The delegate receives it inside the dispatch envelope and must echo it as
80
+ ``report_token`` in the completion report; ``complete`` recomputes and
81
+ compares. Empty string when no key is configured (check disabled)."""
82
+ key = _hmac_key()
83
+ return _sign(key, f"report:{task_id}") if key else ""
84
+
85
+
86
+ class DirectedActionService:
87
+ def __init__(
88
+ self,
89
+ store: ScopedTaskStore,
90
+ directive_manager: Any = None,
91
+ mirrors: Any = None, # RepoMirrorManager (audit + target resolution)
92
+ feedback_store: Any = None,
93
+ delivery_router: Any = None, # awaitable (payload) -> bool, e.g. loop._route_reachout_delivery bound with delivery
94
+ self_model: Any = None, # SelfModel (trust engine + journal, Amendment 1)
95
+ ) -> None:
96
+ self.store = store
97
+ self._directives = directive_manager
98
+ self._mirrors = mirrors
99
+ self._feedback = feedback_store
100
+ self._deliver = delivery_router
101
+ self._self_model = self_model
102
+ # Observability: boundary checks that raised (see intake gate 1).
103
+ self.boundary_check_errors = 0
104
+
105
+ # -- trust helpers (Amendment 1.7) -----------------------------------
106
+ @staticmethod
107
+ def _trust_domain(task: ScopedTask) -> str:
108
+ """Trust domain per scope class: all read-only scopes pool their
109
+ track record; mutating scopes earn trust per approval_key."""
110
+ return f"directed:{task.approval_key}" if task.mutating else "directed:read"
111
+
112
+ def _journal(self, task: ScopedTask, decision: str, why: str,
113
+ confidence: Any = None) -> None:
114
+ journal = getattr(self._self_model, "journal", None)
115
+ if journal is None:
116
+ return
117
+ try:
118
+ journal.record(
119
+ self._trust_domain(task), task.objective[:300],
120
+ reasoning=why, confidence=confidence,
121
+ reversibility="recoverable" if task.mutating else "reversible",
122
+ decision=decision, ref=task.id)
123
+ except Exception:
124
+ logger.debug("directed journal failed", exc_info=True)
125
+
126
+ # -- known targets --------------------------------------------------
127
+ def known_targets(self) -> List[Dict[str, str]]:
128
+ """Owner-designated repos (mirrors) as intake-resolvable targets."""
129
+ out: List[Dict[str, str]] = []
130
+ if self._mirrors is not None:
131
+ try:
132
+ for name, info in self._mirrors.configured().items():
133
+ out.append({"kind": "repo", "name": name,
134
+ "aliases": info.get("aliases", ""),
135
+ "ref": info.get("url", "")})
136
+ except Exception:
137
+ pass
138
+ return out
139
+
140
+ # -- intake + gates --------------------------------------------------
141
+ async def intake(self, directive_text: str,
142
+ source: str = "owner") -> ScopedTask:
143
+ """Turn a directive into a gated ScopedTask (does NOT dispatch)."""
144
+ task = scope_from_directive(directive_text, self.known_targets())
145
+
146
+ # Gate 1: boundaries FIRST -- refuse with the citation.
147
+ if self._directives is not None:
148
+ try:
149
+ from apsimo.directives import Action
150
+ verdict = self._directives.check(Action(
151
+ kind="directed_action",
152
+ text=task.searchable_text(),
153
+ target=",".join(t.get("name", "") for t in task.targets),
154
+ high_risk=True,
155
+ ))
156
+ if not verdict.allowed:
157
+ task.status = "refused"
158
+ task.refusal_reason = verdict.reason
159
+ self.store.save(task)
160
+ logger.warning("Directed intake REFUSED by boundary: %s", verdict.reason)
161
+ return task
162
+ except Exception:
163
+ # An owner boundary we cannot evaluate must not be assumed
164
+ # permissive: fail CLOSED by default and refuse the intake
165
+ # (the owner can simply re-issue the directive).
166
+ self.boundary_check_errors += 1
167
+ from apsimo.directives.guard import boundary_fail_closed
168
+ if boundary_fail_closed():
169
+ task.status = "refused"
170
+ task.refusal_reason = "boundary_check_error"
171
+ self.store.save(task)
172
+ logger.warning(
173
+ "Directed intake REFUSED: boundary_check_error "
174
+ "(boundary check raised; failing closed)",
175
+ exc_info=True,
176
+ )
177
+ return task
178
+ logger.debug("directed boundary check failed (allowing)", exc_info=True)
179
+
180
+ # Gate 2: approval tiering, now trust-graduated (Amendment 1.7).
181
+ # Read-only scopes act with journaling. Mutating scopes may consume a
182
+ # bounded compatibility approval; otherwise the trust engine decides: an
183
+ # earned act_first class self-approves (journaled + reported), an
184
+ # ask_first class asks the owner WITH the reasoning and confidence.
185
+ # The immutable floor always asks.
186
+ if not task.mutating:
187
+ task.approval = {"required": False, "reason": "read_only_auto"}
188
+ task.status = "approved"
189
+ self._journal(task, "acted", "read-only scope auto-approved")
190
+ else:
191
+ standing = False
192
+ try:
193
+ from apsimo.initiatives import standing_approvals
194
+ standing = standing_approvals.is_approved(task.approval_key)
195
+ except Exception:
196
+ pass
197
+ if standing:
198
+ task.approval = {"required": True, "granted_by": "standing",
199
+ "standing": True, "key": task.approval_key}
200
+ task.status = "approved"
201
+ self._journal(task, "acted", "bounded repeat approval")
202
+ else:
203
+ gate = None
204
+ trust = getattr(self._self_model, "trust", None)
205
+ if trust is not None:
206
+ try:
207
+ gate = trust.gate(
208
+ self._trust_domain(task),
209
+ task.objective,
210
+ reasoning=f"owner directive: {task.directive_text[:200]}",
211
+ reversibility="recoverable",
212
+ default_stage="ask_first",
213
+ ref=task.id)
214
+ except Exception:
215
+ logger.debug("directed trust gate failed", exc_info=True)
216
+ if gate is not None and gate["decision"] == "act":
217
+ task.approval = {
218
+ "required": True, "granted_by": "trust_engine",
219
+ "standing": False, "key": task.approval_key,
220
+ "confidence": round(gate["confidence"], 3),
221
+ }
222
+ task.status = "approved"
223
+ else:
224
+ task.approval = {"required": True, "standing": False,
225
+ "key": task.approval_key}
226
+ if gate is not None:
227
+ task.approval["confidence"] = round(gate["confidence"], 3)
228
+ task.status = "awaiting_approval"
229
+ await self._request_approval(task, gate)
230
+ self.store.save(task)
231
+ return task
232
+
233
+ async def _request_approval(self, task: ScopedTask,
234
+ gate: Optional[Dict[str, Any]]) -> None:
235
+ """Ask-first (Amendment 1.1): the approval request carries the
236
+ reasoning and the confidence, through the guarded delivery path."""
237
+ if self._deliver is None:
238
+ return
239
+ try:
240
+ from apsimo.proposals import Proposal, proposal_to_payload
241
+ conf = (gate or {}).get("confidence")
242
+ conf_txt = f" My confidence on this class of work is {conf:.2f}." if conf is not None else ""
243
+ prop = Proposal(
244
+ title=f"Approval needed: {task.objective[:60]}",
245
+ finding=(f"I want to run this directed task: {task.objective[:300]}. "
246
+ f"Scope: ops={','.join(task.allowed_ops)}, "
247
+ f"targets={[t.get('name') for t in task.targets]}, "
248
+ f"branch prefix {task.limits.branch_prefix!r}, "
249
+ f"max {task.limits.max_commits} commits.{conf_txt}"),
250
+ why_it_helps="it is mutating work, so you decide until I have "
251
+ "earned act-first trust on this scope",
252
+ suggested_action=f"Approve with: directed approve {task.id} "
253
+ "(or tell me to drop it)",
254
+ source=task.id, initiative_type="proposal",
255
+ confidence=0.8)
256
+ await self._deliver(proposal_to_payload(prop))
257
+ except Exception:
258
+ logger.debug("approval request delivery failed", exc_info=True)
259
+
260
+ def approve(
261
+ self,
262
+ task_id: str,
263
+ approved_by: str = "trusted-internal",
264
+ standing: bool = False,
265
+ grant_expires_in_seconds: int = 7 * 24 * 60 * 60,
266
+ grant_max_uses: int = 5,
267
+ ) -> Optional[ScopedTask]:
268
+ task = self.store.get(task_id)
269
+ if task is None or task.status != "awaiting_approval":
270
+ return task
271
+ task.approval.update({"granted_by": approved_by, "standing": standing})
272
+ task.status = "approved"
273
+ if standing:
274
+ try:
275
+ from apsimo.initiatives import standing_approvals
276
+ standing_approvals.grant(
277
+ task.approval_key,
278
+ approved_by=approved_by,
279
+ expires_in_seconds=grant_expires_in_seconds,
280
+ max_uses=grant_max_uses,
281
+ )
282
+ except Exception:
283
+ pass
284
+ self.store.save(task)
285
+ return task
286
+
287
+ # -- dispatch ---------------------------------------------------------
288
+ async def dispatch(self, task_id: str) -> Dict[str, Any]:
289
+ """Send an APPROVED ScopedTask to the delegate (or log it in dry-run)."""
290
+ task = self.store.get(task_id)
291
+ if task is None:
292
+ return {"dispatched": False, "reason": "not_found"}
293
+ if task.status != "approved":
294
+ return {"dispatched": False, "reason": f"not_approved (status={task.status})"}
295
+ if task.is_expired():
296
+ task.status = "expired"; self.store.save(task)
297
+ return {"dispatched": False, "reason": "expired"}
298
+
299
+ # Daily dispatch cap (rolling 24h, dry + live both count): bounds the
300
+ # blast radius of a runaway approval/dispatch loop. The task stays
301
+ # approved so the owner loses nothing but time.
302
+ cap = _max_dispatch_per_day()
303
+ if cap > 0:
304
+ recent = 0
305
+ try:
306
+ recent = self.store.dispatches_since(time.time() - 86400.0)
307
+ except Exception:
308
+ logger.debug("dispatch cap count failed (allowing)", exc_info=True)
309
+ if recent >= cap:
310
+ logger.warning(
311
+ "Directed dispatch %s REFUSED: daily cap reached (%d/%d "
312
+ "in 24h; COLONY_DIRECTED_MAX_DISPATCH_PER_DAY)",
313
+ task.id, recent, cap)
314
+ self._journal(task, "held",
315
+ f"daily dispatch cap reached ({recent}/{cap})")
316
+ return {"dispatched": False, "reason": "daily_dispatch_cap",
317
+ "cap": cap, "count": recent}
318
+
319
+ mode = directed_mode()
320
+ # Dispatch envelope: verifiable provenance for the contract. Signed
321
+ # only when COLONY_DIRECTED_HMAC_KEY is set (unset = legacy unsigned).
322
+ envelope: Dict[str, Any] = {
323
+ "task_id": task.id,
324
+ "issued_at": time.time(),
325
+ "nonce": uuid.uuid4().hex,
326
+ }
327
+ key = _hmac_key()
328
+ if key:
329
+ envelope["algo"] = "hmac-sha256"
330
+ envelope["signature"] = _sign(
331
+ key, f"{task.id}.{envelope['issued_at']}.{envelope['nonce']}")
332
+ envelope["report_token"] = report_token_for(task.id)
333
+ payload = {
334
+ "type": "directed_task",
335
+ "task": task.to_dict(),
336
+ "contract": {
337
+ "work_branch_prefix": task.limits.branch_prefix,
338
+ "instructions": (
339
+ "Work ONLY within the attached scope. Create a dedicated "
340
+ f"branch under '{task.limits.branch_prefix}'. Never force-push, "
341
+ "never delete outside scope, respect the commit cap. On "
342
+ "completion POST a structured report (summary, operations, "
343
+ "files_touched, commits, branch) to report_url."
344
+ ),
345
+ },
346
+ "report_url": f"/v1/host/directed/tasks/{task.id}/report",
347
+ "envelope": envelope,
348
+ }
349
+ if mode != "live":
350
+ logger.info(
351
+ "DRY-RUN directed dispatch %s -> %s | ops=%s targets=%s "
352
+ "limits={branch_prefix:%s,max_commits:%d} mutating=%s",
353
+ task.id, _dispatch_url() or "(no endpoint configured)",
354
+ task.allowed_ops, [t.get("name") for t in task.targets],
355
+ task.limits.branch_prefix, task.limits.max_commits, task.mutating,
356
+ )
357
+ task.status = "dispatched_dry"
358
+ self.store.save(task)
359
+ try:
360
+ self.store.log_dispatch(task.id, mode)
361
+ except Exception:
362
+ logger.debug("dispatch log failed", exc_info=True)
363
+ return {"dispatched": False, "dry_run": True, "payload": payload}
364
+
365
+ url = _dispatch_url()
366
+ if not url:
367
+ return {"dispatched": False, "reason": "no_dispatch_endpoint"}
368
+ try:
369
+ import aiohttp
370
+ headers = {"Content-Type": "application/json"}
371
+ api_key = os.environ.get("COLONY_API_KEY", "")
372
+ if api_key:
373
+ headers["Authorization"] = f"Bearer {api_key}"
374
+ if key:
375
+ headers["X-Colony-Directed-Signature"] = envelope["signature"]
376
+ async with aiohttp.ClientSession() as s:
377
+ async with s.post(url, json=payload, headers=headers,
378
+ timeout=aiohttp.ClientTimeout(total=10)) as resp:
379
+ ok = resp.status in (200, 202)
380
+ except Exception as exc:
381
+ logger.warning("directed dispatch failed: %s", exc)
382
+ ok = False
383
+ task.status = "dispatched" if ok else "failed"
384
+ self.store.save(task)
385
+ if ok:
386
+ try:
387
+ self.store.log_dispatch(task.id, mode)
388
+ except Exception:
389
+ logger.debug("dispatch log failed", exc_info=True)
390
+ return {"dispatched": ok, "url": url}
391
+
392
+ # -- report-back + audit ----------------------------------------------
393
+ def _reject_report(self, task: ScopedTask, reason: str) -> Dict[str, Any]:
394
+ """Reject a report-back loudly: journaled WARNING, nothing recorded."""
395
+ logger.warning(
396
+ "Directed report for %s REJECTED (%s): task status=%s "
397
+ "(strict reports; COLONY_DIRECTED_STRICT_REPORTS=0 for legacy)",
398
+ task.id, reason, task.status)
399
+ self._journal(task, "blocked",
400
+ f"report-back rejected: {reason} (status={task.status})")
401
+ return {"ok": False, "reason": reason, "status": task.status}
402
+
403
+ async def complete(self, task_id: str, report: Dict[str, Any]) -> Dict[str, Any]:
404
+ """Delegate report-back: audit vs scope, record outcome, notify owner.
405
+
406
+ Strict mode (default) closes a real hole — a report used to be
407
+ accepted for a task in ANY status, letting a stray or duplicate POST
408
+ fabricate audited outcomes (and trust evidence) for work that was
409
+ never dispatched. Now only ``dispatched`` tasks take a report; a
410
+ ``dispatched_dry`` task takes a dry echo that records no trust
411
+ outcome; anything else is rejected with a journaled WARNING.
412
+ """
413
+ task = self.store.get(task_id)
414
+ if task is None:
415
+ return {"ok": False, "reason": "not_found"}
416
+
417
+ report = report or {}
418
+ dry_echo = False
419
+ if _strict_reports():
420
+ # Envelope report token (only enforced when a key is configured).
421
+ expected = report_token_for(task.id)
422
+ if expected and not hmac.compare_digest(
423
+ str(report.get("report_token", "")), expected):
424
+ return self._reject_report(task, "bad_report_token")
425
+ if task.status in ("completed", "violated"):
426
+ return self._reject_report(task, "double_report")
427
+ if task.status == "dispatched_dry":
428
+ dry_echo = True
429
+ elif task.status != "dispatched":
430
+ return self._reject_report(task, "status_mismatch")
431
+
432
+ mirror_path = None
433
+ if self._mirrors is not None and task.targets:
434
+ try:
435
+ mirror_path = self._mirrors.path_for(task.targets[0].get("name", ""))
436
+ except Exception:
437
+ mirror_path = None
438
+
439
+ audit = audit_completion(task, report, mirror_path=mirror_path)
440
+ task.audit = audit
441
+ verdict = audit["verdict"]
442
+ task.status = "violated" if verdict == "violation" else "completed"
443
+ self.store.save(task)
444
+
445
+ # Outcome feedback.
446
+ if self._feedback is not None:
447
+ try:
448
+ self._feedback.record(
449
+ "directed_action",
450
+ "actioned" if verdict == "clean" else "dismissed",
451
+ )
452
+ except Exception:
453
+ pass
454
+
455
+ # Trust engine: audited outcomes are the earned-autonomy evidence
456
+ # (Amendment 1.7); a violation trips the circuit breaker. An
457
+ # unverified MUTATING completion is recorded as nothing: it must
458
+ # neither graduate a scope class nor trip its breaker. A dry echo
459
+ # (report against a dry-run dispatch) records NO trust outcome —
460
+ # nothing real happened, so nothing may graduate or demote.
461
+ if self._self_model is not None and not dry_echo:
462
+ try:
463
+ if verdict == "violation":
464
+ self._self_model.record(self._trust_domain(task),
465
+ "failure", violation=True)
466
+ elif verdict == "clean" or not task.mutating:
467
+ self._self_model.record(self._trust_domain(task),
468
+ "success")
469
+ except Exception:
470
+ pass
471
+ self._journal(task, "noted",
472
+ f"completion audited: {verdict}")
473
+
474
+ # Owner-facing report through the guarded reach-out path (still held
475
+ # by delivery shadow until go-live).
476
+ await self._notify_owner(task, report, audit)
477
+ out = {"ok": True, "verdict": verdict, "audit": audit}
478
+ if dry_echo:
479
+ out["dry_echo"] = True
480
+ return out
481
+
482
+ async def _notify_owner(self, task: ScopedTask, report: Dict[str, Any],
483
+ audit: Dict[str, Any]) -> None:
484
+ if self._deliver is None:
485
+ return
486
+ try:
487
+ from apsimo.proposals import Proposal, proposal_to_payload
488
+ verdict = audit.get("verdict", "unverified")
489
+ if verdict == "violation":
490
+ findings = "; ".join(
491
+ audit.get("report_audit", {}).get("findings", [])[:5])
492
+ title = f"SCOPE VIOLATION on directed task: {task.objective[:60]}"
493
+ finding = (f"The delegate exceeded the granted scope: {findings}. "
494
+ "I flagged and recorded it; nothing was accepted.")
495
+ urgency = 0.95
496
+ else:
497
+ title = f"Directed task {verdict}: {task.objective[:60]}"
498
+ finding = str(report.get("summary", ""))[:600] or "Task completed."
499
+ urgency = 0.7
500
+ prop = Proposal(
501
+ title=title, finding=finding,
502
+ why_it_helps="closes the loop on the work you directed",
503
+ suggested_action=("Review the flagged changes" if verdict == "violation"
504
+ else "Review the result; tell me any follow-ups"),
505
+ source=task.id, initiative_type="proposal", confidence=urgency,
506
+ )
507
+ await self._deliver(proposal_to_payload(prop))
508
+ except Exception:
509
+ logger.debug("directed owner-notify failed", exc_info=True)
@@ -0,0 +1,25 @@
1
+ """Directive / boundary memory: durable owner boundaries + enforcement.
2
+
3
+ Public surface:
4
+ DirectiveManager -- capture + store + enforce (single entry point)
5
+ DirectiveStore -- SQLite persistence
6
+ DirectiveGuard -- boundary enforcement (check an Action -> Verdict)
7
+ Action, Verdict -- the enforcement contract
8
+ Directive, Polarity, DirectiveStatus -- the data model
9
+ """
10
+
11
+ from apsimo.directives.models import (
12
+ Directive, Polarity, DirectiveStatus, normalize_terms,
13
+ )
14
+ from apsimo.directives.store import DirectiveStore
15
+ from apsimo.directives.guard import (
16
+ DirectiveGuard, DirectiveStoreUnavailable, Action, Verdict,
17
+ boundary_fail_closed,
18
+ )
19
+ from apsimo.directives.service import DirectiveManager
20
+
21
+ __all__ = [
22
+ "Directive", "Polarity", "DirectiveStatus", "normalize_terms",
23
+ "DirectiveStore", "DirectiveGuard", "DirectiveStoreUnavailable", "Action",
24
+ "Verdict", "DirectiveManager", "boundary_fail_closed",
25
+ ]
@@ -0,0 +1,87 @@
1
+ """Canonical source bindings for automatically learned standing directives.
2
+
3
+ Only references persist in the existing directive metadata column. Current
4
+ source bytes supply the exact rule; correction or erasure withdraws its effect.
5
+ """
6
+ from contextlib import closing
7
+ import hashlib
8
+ import json
9
+
10
+ from apsimo.turns.idempotency import canonical_turn_digest, source_message_hash
11
+ from apsimo.turns.audio import source_text
12
+
13
+
14
+ def _message(ledger, reference):
15
+ if ledger is None:
16
+ return None
17
+ identifier = reference['source_turn_id']
18
+ if ledger.is_projection_erased(identifier):
19
+ return None
20
+ with closing(ledger._connect()) as db:
21
+ row = db.execute("SELECT * FROM turn_sources WHERE turn_id=? AND contact_id=? AND scope='person'",
22
+ (identifier, reference['contact_id'])).fetchone()
23
+ if row is None:
24
+ return None
25
+ from apsimo.turns.source_attribution import is_invalidated
26
+ if is_invalidated(db, identifier):
27
+ return None
28
+ messages = json.loads(row['messages_json'])
29
+ if canonical_turn_digest(messages) != reference['source_version']:
30
+ return None
31
+ message_hash = reference['message_hash']
32
+ # Existing correction annotations are tied to exact source messages.
33
+ if db.execute('SELECT 1 FROM source_annotations a,json_each(a.target_message_hashes_json) h '
34
+ 'WHERE a.target_source_id=? AND h.value=? LIMIT 1', (identifier, message_hash)).fetchone():
35
+ return None
36
+ for message in messages:
37
+ if message.get('role') == 'user' and source_message_hash(row['session_id'], message) == message_hash:
38
+ return source_text(message.get('content'))
39
+ return None
40
+
41
+
42
+ def bind(ledger, *, source_id, contact_id, message, clause):
43
+ if ledger is None or not source_id or not contact_id:
44
+ return None
45
+ with closing(ledger._connect()) as db:
46
+ row = db.execute("SELECT * FROM turn_sources WHERE turn_id=? AND contact_id=? AND scope='person'",
47
+ (source_id, contact_id)).fetchone()
48
+ if row is None:
49
+ return None
50
+ messages = json.loads(row['messages_json'])
51
+ for item in messages:
52
+ text = source_text(item.get('content'))
53
+ if item.get('role') != 'user' or text.strip() != message.strip():
54
+ continue
55
+ start = text.find(clause)
56
+ if start < 0:
57
+ continue
58
+ reference = {'source_turn_id': source_id, 'contact_id': contact_id,
59
+ 'source_version': canonical_turn_digest(messages),
60
+ 'message_hash': source_message_hash(row['session_id'], item),
61
+ 'start': start, 'end': start+len(clause),
62
+ 'clause_sha256': hashlib.sha256(clause.encode()).hexdigest()}
63
+ if _message(ledger, reference) is not None:
64
+ return reference
65
+ return None
66
+
67
+
68
+ def hydrate(ledger, directive):
69
+ reference = directive.evidence
70
+ if reference is None:
71
+ return directive # Explicit manual/configuration and legacy rows.
72
+ text = _message(ledger, reference)
73
+ if text is None:
74
+ return None
75
+ clause = text[reference['start']:reference['end']]
76
+ if hashlib.sha256(clause.encode()).hexdigest() != reference['clause_sha256']:
77
+ return None
78
+ from .extractor import extract_directives, is_revocation
79
+ # Re-evaluate the full current source scope, not just a detached quotation.
80
+ rules = [item for item in extract_directives(text) if not is_revocation(item)
81
+ and item.raw_text == clause and item.polarity == directive.polarity]
82
+ if len(rules) != 1:
83
+ return None
84
+ current = rules[0]
85
+ directive.subject, directive.raw_text = current.subject, current.raw_text
86
+ directive.match_terms, directive.level = current.match_terms, current.level
87
+ return directive