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,1708 @@
1
+ """Durable, generic execution boundary for owner-governed Colony actions.
2
+
3
+ This module deliberately knows nothing about conversational context. One
4
+ dedicated API principal supplies the owner/participant binding, while the
5
+ request supplies only an immutable, already-approved action description.
6
+
7
+ The ledger uses a conservative one-mutation-at-most protocol:
8
+
9
+ ``prepared -> executing -> completed``
10
+
11
+ Any exception after ``executing`` is durable ``ambiguous`` and can never be
12
+ retried as a mutation. An ``executing`` row found at process startup is also
13
+ recovered as ``ambiguous``. This intentionally prefers an honest uncertain
14
+ result over accidentally applying an effect twice.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import asyncio
20
+ import hashlib
21
+ import inspect
22
+ import json
23
+ import logging
24
+ import math
25
+ import os
26
+ from pathlib import Path
27
+ import re
28
+ import sqlite3
29
+ import stat
30
+ import threading
31
+ import time
32
+ from typing import Any, Mapping
33
+ import uuid
34
+
35
+ from apsimo.api.authority import RequestAuthority
36
+
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+ GOVERNED_ACTION_PRINCIPAL = "host-action-worker"
41
+ GOVERNED_ACTION_SCOPES = frozenset({"actions:execute", "actions:verify"})
42
+ ACTION_TOOL_NAMES = frozenset(
43
+ {
44
+ "colony_autonomy_disable",
45
+ "colony_autonomy_enable",
46
+ "colony_create_commitment",
47
+ "colony_initiative_feedback",
48
+ "colony_record_insight",
49
+ "colony_research",
50
+ "colony_resolve_commitment",
51
+ "colony_task_complete",
52
+ "colony_task_dismiss",
53
+ "colony_task_snooze",
54
+ }
55
+ )
56
+
57
+ GOVERNED_ACTION_REQUEST_MAX_BYTES = 32 * 1024
58
+ _REQUEST_MAX = GOVERNED_ACTION_REQUEST_MAX_BYTES
59
+ _REQUEST_FIELDS = frozenset(
60
+ {
61
+ "schema",
62
+ "version",
63
+ "action_id",
64
+ "action_digest",
65
+ "intent_id",
66
+ "intent_digest",
67
+ "tool_name",
68
+ "args",
69
+ "args_sha256",
70
+ "approval",
71
+ "execution_digest",
72
+ }
73
+ )
74
+ _APPROVAL_FIELDS = frozenset(
75
+ {
76
+ "schema",
77
+ "version",
78
+ "approval_id",
79
+ "decision_id",
80
+ "revision",
81
+ "authorization_receipt_sha256",
82
+ "decided_at",
83
+ "expires_at",
84
+ }
85
+ )
86
+ _EFFECT_FIELDS = frozenset(
87
+ {"schema", "version", "effect_id", "outcome", "verification"}
88
+ )
89
+ _SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
90
+ _INTENT_ID_RE = re.compile(r"^hti_[0-9a-f]{32}$")
91
+ _APPROVAL_ID_RE = re.compile(r"^APR-[A-Z0-9]{12}$")
92
+ _SAFE_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,255}$")
93
+ GOVERNED_IDENTIFIER_PATTERN = (
94
+ r"^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}(?![\s\S])"
95
+ )
96
+ _REQUEST_ID_RE = re.compile(GOVERNED_IDENTIFIER_PATTERN)
97
+ _STATES = frozenset(
98
+ {"prepared", "executing", "completed", "failed", "ambiguous"}
99
+ )
100
+ GOVERNED_RESEARCH_TOPIC_MAX_CHARS = 1400
101
+ GOVERNED_COMMITMENT_PRIORITY_DEFAULT = 60
102
+ GOVERNED_COMMITMENT_DESCRIPTION_MAX_CHARS = 8000
103
+ GOVERNED_COMMITMENT_DUE_AT_MAX_CHARS = 256
104
+ GOVERNED_INSIGHT_CONTENT_MAX_CHARS = 16000
105
+ GOVERNED_FREEFORM_REASON_MAX_CHARS = 8000
106
+ GOVERNED_IDENTIFIER_MAX_CHARS = 256
107
+ GOVERNED_DETAILS_MAX_NODES = 512
108
+ GOVERNED_DETAILS_MAX_DEPTH = 8
109
+ GOVERNED_DETAILS_STRING_MAX_CHARS = 4096
110
+ GOVERNED_DETAILS_KEY_MAX_CHARS = 128
111
+ GOVERNED_DETAILS_INTEGER_MAX = (1 << 63) - 1
112
+
113
+
114
+ class GovernedActionValidationError(ValueError):
115
+ """The external execution document is not the exact bounded contract."""
116
+
117
+
118
+ class GovernedActionConflict(ValueError):
119
+ """An action identifier is already bound to a different request."""
120
+
121
+
122
+ class GovernedActionNotFound(LookupError):
123
+ """No durable execution record exists for the requested action."""
124
+
125
+
126
+ class PrivateGovernedActionLedgerError(OSError):
127
+ """The governed-action ledger path is not owner-private and stable."""
128
+
129
+
130
+ def canonical_json(value: Any) -> str:
131
+ try:
132
+ return json.dumps(
133
+ value,
134
+ sort_keys=True,
135
+ separators=(",", ":"),
136
+ ensure_ascii=True,
137
+ allow_nan=False,
138
+ )
139
+ except (TypeError, ValueError, OverflowError, RecursionError) as exc:
140
+ raise GovernedActionValidationError("value is not canonical JSON") from exc
141
+
142
+
143
+ def sha256_json(value: Any) -> str:
144
+ return hashlib.sha256(canonical_json(value).encode("utf-8")).hexdigest()
145
+
146
+
147
+ def _wire_sha256_json(value: Any) -> str:
148
+ """Hash the host Action Plane wire form without changing local storage.
149
+
150
+ The host's args/intent digests intentionally use the historical ASCII-escaped
151
+ canonical form above. Only its outer execution envelope and returned
152
+ effect digest use canonical UTF-8 JSON, so those two fields call this
153
+ narrowly scoped helper.
154
+ """
155
+
156
+ try:
157
+ encoded = json.dumps(
158
+ value,
159
+ sort_keys=True,
160
+ separators=(",", ":"),
161
+ ensure_ascii=False,
162
+ allow_nan=False,
163
+ ).encode("utf-8")
164
+ except (TypeError, ValueError, OverflowError, RecursionError) as exc:
165
+ raise GovernedActionValidationError("value is not canonical JSON") from exc
166
+ return hashlib.sha256(encoded).hexdigest()
167
+
168
+
169
+ class _FrozenJSONDict(dict):
170
+ """JSON-serializable mapping that refuses in-place authority changes."""
171
+
172
+ @staticmethod
173
+ def _immutable(*_args, **_kwargs):
174
+ raise TypeError("governed execution context is immutable")
175
+
176
+ __setitem__ = _immutable
177
+ __delitem__ = _immutable
178
+ clear = _immutable
179
+ pop = _immutable
180
+ popitem = _immutable
181
+ setdefault = _immutable
182
+ update = _immutable
183
+ __ior__ = _immutable
184
+
185
+
186
+ def _freeze_json(value: Any) -> Any:
187
+ if isinstance(value, Mapping):
188
+ return _FrozenJSONDict({
189
+ key: _freeze_json(item) for key, item in value.items()
190
+ })
191
+ if isinstance(value, list):
192
+ return tuple(_freeze_json(item) for item in value)
193
+ return value
194
+
195
+
196
+ def _strict_json(raw: bytes, *, maximum: int = _REQUEST_MAX) -> Any:
197
+ if not isinstance(raw, bytes) or not raw or len(raw) > maximum:
198
+ raise GovernedActionValidationError("execution document is outside its size bound")
199
+
200
+ def pairs(values):
201
+ result = {}
202
+ for key, value in values:
203
+ if key in result:
204
+ raise GovernedActionValidationError("JSON object keys must be unique")
205
+ result[key] = value
206
+ return result
207
+
208
+ def constant(_value):
209
+ raise GovernedActionValidationError("JSON numbers must be finite")
210
+
211
+ def integer(value):
212
+ if len(value.lstrip("-")) > 19:
213
+ raise GovernedActionValidationError("JSON integer is outside its bound")
214
+ parsed = int(value)
215
+ if abs(parsed) > (1 << 63) - 1:
216
+ raise GovernedActionValidationError("JSON integer is outside its bound")
217
+ return parsed
218
+
219
+ def floating(value):
220
+ parsed = float(value)
221
+ if not math.isfinite(parsed):
222
+ raise GovernedActionValidationError("JSON numbers must be finite")
223
+ return parsed
224
+
225
+ try:
226
+ value = json.loads(
227
+ raw.decode("utf-8", errors="strict"),
228
+ object_pairs_hook=pairs,
229
+ parse_constant=constant,
230
+ parse_int=integer,
231
+ parse_float=floating,
232
+ )
233
+ except GovernedActionValidationError:
234
+ raise
235
+ except (UnicodeError, ValueError, TypeError, OverflowError, RecursionError) as exc:
236
+ raise GovernedActionValidationError("execution document is malformed") from exc
237
+
238
+ stack = [(value, 0)]
239
+ count = 0
240
+ while stack:
241
+ item, depth = stack.pop()
242
+ count += 1
243
+ if count > 1024 or depth > 16:
244
+ raise GovernedActionValidationError("execution document is too complex")
245
+ if isinstance(item, str):
246
+ if len(item) > 16_000 or any(
247
+ ord(character) == 0 or 0xD800 <= ord(character) <= 0xDFFF
248
+ for character in item
249
+ ):
250
+ raise GovernedActionValidationError("execution text is unsafe")
251
+ elif isinstance(item, Mapping):
252
+ stack.extend((key, depth + 1) for key in item)
253
+ stack.extend((child, depth + 1) for child in item.values())
254
+ elif isinstance(item, list):
255
+ stack.extend((child, depth + 1) for child in item)
256
+ elif item is not None and not isinstance(item, (bool, int, float)):
257
+ raise GovernedActionValidationError("execution document contains a non-JSON value")
258
+ return value
259
+
260
+
261
+ def _exact_mapping(value: Any, fields, *, name: str) -> dict[str, Any]:
262
+ if not isinstance(value, Mapping) or set(value) != set(fields):
263
+ raise GovernedActionValidationError(f"{name} fields are invalid")
264
+ if any(not isinstance(key, str) for key in value):
265
+ raise GovernedActionValidationError(f"{name} keys must be strings")
266
+ return dict(value)
267
+
268
+
269
+ def _text(
270
+ value: Any,
271
+ name: str,
272
+ maximum: int,
273
+ *,
274
+ allow_empty: bool = False,
275
+ identifier: bool = False,
276
+ ) -> str:
277
+ if not isinstance(value, str) or len(value) > maximum:
278
+ raise GovernedActionValidationError(f"{name} must be a bounded string")
279
+ if any(ord(character) == 0 or 0xD800 <= ord(character) <= 0xDFFF for character in value):
280
+ raise GovernedActionValidationError(f"{name} contains invalid characters")
281
+ if not allow_empty and not value.strip():
282
+ raise GovernedActionValidationError(f"{name} cannot be empty")
283
+ if identifier and not _REQUEST_ID_RE.fullmatch(value):
284
+ raise GovernedActionValidationError(f"{name} is not a canonical identifier")
285
+ return value
286
+
287
+
288
+ def _integer(value: Any, name: str, minimum: int, maximum: int) -> int:
289
+ if isinstance(value, bool) or not isinstance(value, int):
290
+ raise GovernedActionValidationError(f"{name} must be an integer")
291
+ if not minimum <= value <= maximum:
292
+ raise GovernedActionValidationError(f"{name} is outside its range")
293
+ return value
294
+
295
+
296
+ def _number(value: Any, name: str, minimum: float, maximum: float) -> int | float:
297
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
298
+ raise GovernedActionValidationError(f"{name} must be a number")
299
+ try:
300
+ finite = math.isfinite(float(value))
301
+ except (ValueError, OverflowError):
302
+ finite = False
303
+ if not finite or not minimum <= value <= maximum:
304
+ raise GovernedActionValidationError(f"{name} is outside its range")
305
+ return value
306
+
307
+
308
+ def _enum(value: Any, name: str, allowed: frozenset[str]) -> str:
309
+ if not isinstance(value, str) or value not in allowed:
310
+ raise GovernedActionValidationError(f"{name} is not an allowed value")
311
+ return value
312
+
313
+
314
+ def _bounded_json(value: Any, name: str, *, depth=0, counter=None) -> Any:
315
+ if counter is None:
316
+ counter = [0]
317
+ counter[0] += 1
318
+ if (
319
+ counter[0] > GOVERNED_DETAILS_MAX_NODES
320
+ or depth > GOVERNED_DETAILS_MAX_DEPTH
321
+ ):
322
+ raise GovernedActionValidationError(f"{name} is too complex")
323
+ if value is None or isinstance(value, bool):
324
+ return value
325
+ if isinstance(value, int):
326
+ if abs(value) > GOVERNED_DETAILS_INTEGER_MAX:
327
+ raise GovernedActionValidationError(f"{name} integer is too large")
328
+ return value
329
+ if isinstance(value, float):
330
+ if not math.isfinite(value):
331
+ raise GovernedActionValidationError(f"{name} number must be finite")
332
+ return value
333
+ if isinstance(value, str):
334
+ return _text(
335
+ value, name, GOVERNED_DETAILS_STRING_MAX_CHARS, allow_empty=True,
336
+ )
337
+ if isinstance(value, list):
338
+ return [
339
+ _bounded_json(item, name, depth=depth + 1, counter=counter)
340
+ for item in value
341
+ ]
342
+ if isinstance(value, Mapping):
343
+ result = {}
344
+ for key, item in value.items():
345
+ key = _text(
346
+ key, f"{name} key", GOVERNED_DETAILS_KEY_MAX_CHARS,
347
+ identifier=True,
348
+ )
349
+ result[key] = _bounded_json(
350
+ item, name, depth=depth + 1, counter=counter
351
+ )
352
+ return result
353
+ raise GovernedActionValidationError(f"{name} contains a non-JSON value")
354
+
355
+
356
+ def _bounded_verification(value: Any, *, depth=0, counter=None) -> Any:
357
+ """Match the host worker's fixed verification projection bounds exactly."""
358
+
359
+ if counter is None:
360
+ counter = [0]
361
+ counter[0] += 1
362
+ if counter[0] > 256 or depth > 6:
363
+ raise RuntimeError("governed action verification is too complex")
364
+ if value is None or isinstance(value, bool):
365
+ return value
366
+ if isinstance(value, int):
367
+ if abs(value) > (1 << 63) - 1:
368
+ raise RuntimeError("governed action verification integer is unsafe")
369
+ return value
370
+ if isinstance(value, float):
371
+ if not math.isfinite(value):
372
+ raise RuntimeError("governed action verification number is unsafe")
373
+ return value
374
+ if isinstance(value, str):
375
+ if len(value) > 2048 or any(
376
+ ord(character) < 0x20 or ord(character) == 0x7F
377
+ for character in value
378
+ ):
379
+ raise RuntimeError("governed action verification text is unsafe")
380
+ return value
381
+ if isinstance(value, list):
382
+ return [
383
+ _bounded_verification(item, depth=depth + 1, counter=counter)
384
+ for item in value
385
+ ]
386
+ if isinstance(value, Mapping):
387
+ result = {}
388
+ for key, item in value.items():
389
+ if not isinstance(key, str) or not _SAFE_ID_RE.fullmatch(key):
390
+ raise RuntimeError("governed action verification key is unsafe")
391
+ result[key] = _bounded_verification(
392
+ item, depth=depth + 1, counter=counter
393
+ )
394
+ return result
395
+ raise RuntimeError("governed action verification is not JSON")
396
+
397
+
398
+ def _validate_args(tool_name: str, raw: Any) -> dict[str, Any]:
399
+ if tool_name in {"colony_autonomy_disable", "colony_autonomy_enable"}:
400
+ return _exact_mapping(raw, set(), name="args")
401
+ if tool_name == "colony_create_commitment":
402
+ args = _exact_mapping(
403
+ raw,
404
+ set(raw) if isinstance(raw, Mapping) else set(),
405
+ name="args",
406
+ )
407
+ if set(args) - {"description", "due_at", "priority"} or "description" not in args:
408
+ raise GovernedActionValidationError("args fields are invalid")
409
+ _text(
410
+ args["description"], "description",
411
+ GOVERNED_COMMITMENT_DESCRIPTION_MAX_CHARS,
412
+ )
413
+ if "due_at" in args:
414
+ _text(
415
+ args["due_at"], "due_at", GOVERNED_COMMITMENT_DUE_AT_MAX_CHARS,
416
+ allow_empty=True,
417
+ )
418
+ if "priority" in args:
419
+ _integer(args["priority"], "priority", 0, 100)
420
+ return args
421
+ if tool_name == "colony_initiative_feedback":
422
+ if not isinstance(raw, Mapping):
423
+ raise GovernedActionValidationError("args must be an object")
424
+ args = dict(raw)
425
+ if (
426
+ set(args) - {"initiative_id", "action", "details"}
427
+ or not {"initiative_id", "action"} <= set(args)
428
+ ):
429
+ raise GovernedActionValidationError("args fields are invalid")
430
+ _text(
431
+ args["initiative_id"], "initiative_id", GOVERNED_IDENTIFIER_MAX_CHARS,
432
+ identifier=True,
433
+ )
434
+ _enum(
435
+ args["action"],
436
+ "action",
437
+ frozenset({"acknowledged", "actioned", "dismissed", "snoozed"}),
438
+ )
439
+ if "details" in args:
440
+ if not isinstance(args["details"], Mapping):
441
+ raise GovernedActionValidationError("details must be an object")
442
+ args["details"] = _bounded_json(args["details"], "details")
443
+ return args
444
+ if tool_name == "colony_record_insight":
445
+ if not isinstance(raw, Mapping):
446
+ raise GovernedActionValidationError("args must be an object")
447
+ args = dict(raw)
448
+ if (
449
+ set(args) - {"confidence", "content", "insight_type"}
450
+ or not {"content", "insight_type"} <= set(args)
451
+ ):
452
+ raise GovernedActionValidationError("args fields are invalid")
453
+ _text(
454
+ args["content"], "content", GOVERNED_INSIGHT_CONTENT_MAX_CHARS,
455
+ )
456
+ _enum(
457
+ args["insight_type"],
458
+ "insight_type",
459
+ frozenset(
460
+ {
461
+ "preference",
462
+ "connection",
463
+ "fact",
464
+ "goal_hint",
465
+ "relationship_update",
466
+ }
467
+ ),
468
+ )
469
+ if "confidence" in args:
470
+ _number(args["confidence"], "confidence", 0.0, 1.0)
471
+ return args
472
+ if tool_name == "colony_research":
473
+ if not isinstance(raw, Mapping):
474
+ raise GovernedActionValidationError("args must be an object")
475
+ args = dict(raw)
476
+ if set(args) - {"depth", "topic"} or "topic" not in args:
477
+ raise GovernedActionValidationError("args fields are invalid")
478
+ _text(
479
+ args["topic"], "topic", GOVERNED_RESEARCH_TOPIC_MAX_CHARS,
480
+ )
481
+ if "depth" in args:
482
+ _enum(args["depth"], "depth", frozenset({"quick", "standard", "deep"}))
483
+ return args
484
+ if tool_name == "colony_resolve_commitment":
485
+ if not isinstance(raw, Mapping):
486
+ raise GovernedActionValidationError("args must be an object")
487
+ args = dict(raw)
488
+ if set(args) - {"commitment_id", "outcome", "reason"} or "commitment_id" not in args:
489
+ raise GovernedActionValidationError("args fields are invalid")
490
+ _text(
491
+ args["commitment_id"], "commitment_id", GOVERNED_IDENTIFIER_MAX_CHARS,
492
+ identifier=True,
493
+ )
494
+ if "outcome" in args:
495
+ _enum(
496
+ args["outcome"],
497
+ "outcome",
498
+ frozenset({"done", "invalid", "duplicate", "wont_do", "obsolete"}),
499
+ )
500
+ if "reason" in args:
501
+ _text(
502
+ args["reason"], "reason", GOVERNED_FREEFORM_REASON_MAX_CHARS,
503
+ allow_empty=True,
504
+ )
505
+ return args
506
+ if tool_name == "colony_task_complete":
507
+ args = _exact_mapping(raw, {"task_id"}, name="args")
508
+ _text(
509
+ args["task_id"], "task_id", GOVERNED_IDENTIFIER_MAX_CHARS,
510
+ identifier=True,
511
+ )
512
+ return args
513
+ if tool_name == "colony_task_dismiss":
514
+ if not isinstance(raw, Mapping):
515
+ raise GovernedActionValidationError("args must be an object")
516
+ args = dict(raw)
517
+ if set(args) - {"task_id", "reason"} or "task_id" not in args:
518
+ raise GovernedActionValidationError("args fields are invalid")
519
+ _text(
520
+ args["task_id"], "task_id", GOVERNED_IDENTIFIER_MAX_CHARS,
521
+ identifier=True,
522
+ )
523
+ if "reason" in args:
524
+ _enum(
525
+ args["reason"],
526
+ "reason",
527
+ frozenset({"stale", "completed", "abandoned", "not_applicable"}),
528
+ )
529
+ return args
530
+ if tool_name == "colony_task_snooze":
531
+ if not isinstance(raw, Mapping):
532
+ raise GovernedActionValidationError("args must be an object")
533
+ args = dict(raw)
534
+ if set(args) - {"task_id", "hours", "reason"} or "task_id" not in args:
535
+ raise GovernedActionValidationError("args fields are invalid")
536
+ _text(
537
+ args["task_id"], "task_id", GOVERNED_IDENTIFIER_MAX_CHARS,
538
+ identifier=True,
539
+ )
540
+ if "hours" in args:
541
+ _integer(args["hours"], "hours", 1, 168)
542
+ if "reason" in args:
543
+ _text(
544
+ args["reason"], "reason", GOVERNED_FREEFORM_REASON_MAX_CHARS,
545
+ allow_empty=True,
546
+ )
547
+ return args
548
+ raise GovernedActionValidationError("tool is not a governed Colony action")
549
+
550
+
551
+ def _valid_action_id(value: Any) -> str:
552
+ if not isinstance(value, str):
553
+ raise GovernedActionValidationError("action_id is invalid")
554
+ try:
555
+ parsed = uuid.UUID(value)
556
+ except (ValueError, AttributeError) as exc:
557
+ raise GovernedActionValidationError("action_id is invalid") from exc
558
+ if parsed.version != 4 or str(parsed) != value:
559
+ raise GovernedActionValidationError("action_id must be a canonical UUIDv4")
560
+ return value
561
+
562
+
563
+ def _digest(value: Any, name: str) -> str:
564
+ if not isinstance(value, str) or not _SHA256_RE.fullmatch(value):
565
+ raise GovernedActionValidationError(f"{name} is invalid")
566
+ return value
567
+
568
+
569
+ def parse_execution_request(
570
+ raw: bytes,
571
+ *,
572
+ path_action_id: str,
573
+ ) -> dict[str, Any]:
574
+ document = _exact_mapping(_strict_json(raw), _REQUEST_FIELDS, name="execution")
575
+ if (
576
+ document["schema"] != "ColonyGovernedActionExecutionV1"
577
+ or isinstance(document["version"], bool)
578
+ or document["version"] != 1
579
+ ):
580
+ raise GovernedActionValidationError("execution schema is invalid")
581
+ action_id = _valid_action_id(document["action_id"])
582
+ if action_id != _valid_action_id(path_action_id):
583
+ raise GovernedActionValidationError("URL action_id does not match the execution")
584
+ _digest(document["action_digest"], "action_digest")
585
+ _digest(document["intent_digest"], "intent_digest")
586
+ _digest(document["args_sha256"], "args_sha256")
587
+ _digest(document["execution_digest"], "execution_digest")
588
+ if not isinstance(document["intent_id"], str) or not _INTENT_ID_RE.fullmatch(
589
+ document["intent_id"]
590
+ ):
591
+ raise GovernedActionValidationError("intent_id is invalid")
592
+ tool_name = document["tool_name"]
593
+ if not isinstance(tool_name, str) or tool_name not in ACTION_TOOL_NAMES:
594
+ raise GovernedActionValidationError("tool is not a governed Colony action")
595
+ args = _validate_args(tool_name, document["args"])
596
+ if document["args_sha256"] != sha256_json(args):
597
+ raise GovernedActionValidationError("args digest does not match")
598
+
599
+ approval = _exact_mapping(document["approval"], _APPROVAL_FIELDS, name="approval")
600
+ if (
601
+ approval["schema"] != "ColonyOwnerApprovalExecutionBindingV1"
602
+ or isinstance(approval["version"], bool)
603
+ or approval["version"] != 1
604
+ or not isinstance(approval["approval_id"], str)
605
+ or not _APPROVAL_ID_RE.fullmatch(approval["approval_id"])
606
+ or isinstance(approval["revision"], bool)
607
+ or approval["revision"] != 1
608
+ ):
609
+ raise GovernedActionValidationError("approval binding is invalid")
610
+ _text(approval["decision_id"], "decision_id", 128, identifier=True)
611
+ _digest(
612
+ approval["authorization_receipt_sha256"],
613
+ "authorization_receipt_sha256",
614
+ )
615
+ for field in ("decided_at", "expires_at"):
616
+ value = approval[field]
617
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
618
+ raise GovernedActionValidationError("approval time is invalid")
619
+ try:
620
+ finite = math.isfinite(float(value))
621
+ except (ValueError, OverflowError):
622
+ finite = False
623
+ if not finite or float(value) <= 0:
624
+ raise GovernedActionValidationError("approval time is invalid")
625
+ decided_at = float(approval["decided_at"])
626
+ expires_at = float(approval["expires_at"])
627
+ if expires_at <= decided_at or expires_at - decided_at > 86_400:
628
+ raise GovernedActionValidationError("approval lifetime is invalid")
629
+
630
+ normalized = dict(document)
631
+ normalized["args"] = args
632
+ normalized["approval"] = approval
633
+ unsigned = {key: value for key, value in normalized.items() if key != "execution_digest"}
634
+ if normalized["execution_digest"] != _wire_sha256_json(unsigned):
635
+ raise GovernedActionValidationError("execution digest does not match")
636
+ # One final canonicalization catches exotic mapping/value behavior and is
637
+ # also the exact document persisted in the ledger.
638
+ canonical_json(normalized)
639
+ return normalized
640
+
641
+
642
+ def _clock_value(clock) -> float:
643
+ try:
644
+ now = float(clock())
645
+ except (TypeError, ValueError, OverflowError) as exc:
646
+ raise RuntimeError("governed action clock is unavailable") from exc
647
+ if not math.isfinite(now) or now <= 0:
648
+ raise RuntimeError("governed action clock is unavailable")
649
+ return now
650
+
651
+
652
+ def _assert_live_approval(request: Mapping[str, Any], now: float) -> None:
653
+ approval = request["approval"]
654
+ if float(approval["decided_at"]) > now + 30 or float(approval["expires_at"]) <= now:
655
+ raise GovernedActionValidationError("approval binding is not live")
656
+
657
+
658
+ def _owner_from_authority(
659
+ authority: RequestAuthority,
660
+ *,
661
+ required_scope: str,
662
+ ) -> str:
663
+ if (
664
+ not isinstance(authority, RequestAuthority)
665
+ or not authority.authenticated
666
+ or authority.legacy
667
+ or authority.anonymous
668
+ or authority.principal_id != GOVERNED_ACTION_PRINCIPAL
669
+ or not authority.credential_id
670
+ or authority.allow_unscoped_api is not False
671
+ or authority.scopes != GOVERNED_ACTION_SCOPES
672
+ or required_scope not in authority.scopes
673
+ or authority.audiences != frozenset({"owner"})
674
+ or not authority.viewer_person_id
675
+ or authority.viewer_person_id not in authority.person_ids
676
+ ):
677
+ raise PermissionError("exact owner-bound governed-action principal required")
678
+ return authority.viewer_person_id
679
+
680
+
681
+ def _status_text(value: Any) -> str:
682
+ raw = getattr(value, "value", value)
683
+ text = str(raw or "unknown")
684
+ return text if _SAFE_ID_RE.fullmatch(text) else "unknown"
685
+
686
+
687
+ def _effect_id(kind: str, value: Any) -> str:
688
+ text = str(value or "")
689
+ if _SAFE_ID_RE.fullmatch(text):
690
+ return text
691
+ digest = hashlib.sha256(text.encode("utf-8", errors="replace")).hexdigest()[:32]
692
+ return f"{kind}-{digest}"
693
+
694
+
695
+ def _validated_effect(value: Any) -> dict[str, Any]:
696
+ effect = _exact_mapping(value, _EFFECT_FIELDS, name="effect")
697
+ if (
698
+ effect["schema"] != "ColonyGovernedActionEffectV1"
699
+ or isinstance(effect["version"], bool)
700
+ or effect["version"] != 1
701
+ or not isinstance(effect["effect_id"], str)
702
+ or not _SAFE_ID_RE.fullmatch(effect["effect_id"])
703
+ or not isinstance(effect["outcome"], str)
704
+ or not _SAFE_ID_RE.fullmatch(effect["outcome"])
705
+ ):
706
+ raise RuntimeError("governed action effect projection is invalid")
707
+ verification = _bounded_verification(effect["verification"])
708
+ normalized = dict(effect)
709
+ normalized["verification"] = verification
710
+ if len(canonical_json(normalized).encode("utf-8")) > 8 * 1024:
711
+ raise RuntimeError("governed action effect projection is too large")
712
+ return normalized
713
+
714
+
715
+ def _state_result(
716
+ request: Mapping[str, Any],
717
+ *,
718
+ state: str,
719
+ observed_at: float,
720
+ effect: Mapping[str, Any] | None = None,
721
+ ) -> dict[str, Any]:
722
+ if state not in _STATES:
723
+ raise RuntimeError("invalid governed action ledger state")
724
+ if state == "completed":
725
+ if effect is None:
726
+ raise RuntimeError("completed action is missing its effect")
727
+ normalized_effect = _validated_effect(effect)
728
+ effect_state = "performed"
729
+ else:
730
+ normalized_effect = {
731
+ "schema": "ColonyGovernedActionEffectV1",
732
+ "version": 1,
733
+ "effect_id": request["action_id"],
734
+ "outcome": state,
735
+ "verification": {"state": state},
736
+ }
737
+ effect_state = {
738
+ "prepared": "not_started",
739
+ "executing": "uncertain",
740
+ "failed": "not_performed",
741
+ "ambiguous": "uncertain",
742
+ }[state]
743
+ result = {
744
+ "schema": "ColonyGovernedActionExecutionResultV1",
745
+ "version": 1,
746
+ "execution_digest": request["execution_digest"],
747
+ "action_id": request["action_id"],
748
+ "action_digest": request["action_digest"],
749
+ "intent_id": request["intent_id"],
750
+ "intent_digest": request["intent_digest"],
751
+ "tool_name": request["tool_name"],
752
+ "status": state,
753
+ "effect_state": effect_state,
754
+ "effect": normalized_effect,
755
+ "effect_digest": _wire_sha256_json(normalized_effect),
756
+ "observed_at": observed_at,
757
+ }
758
+ encoded = canonical_json(result)
759
+ if len(encoded.encode("utf-8")) > 16 * 1024:
760
+ raise RuntimeError("governed action result is too large")
761
+ # Returning the canonical round-trip keeps the first HTTP projection and
762
+ # every ledger replay identical down to JSON member order.
763
+ return json.loads(encoded)
764
+
765
+
766
+ class _PrivateLedgerPath:
767
+ """Bind SQLite to one no-follow, owner-private local path.
768
+
769
+ The immediate parent is an exact mode-0700 current-user directory, so
770
+ SQLite's transient rollback journal remains private regardless of process
771
+ umask. The database is an atomically created or pre-existing mode-0600,
772
+ current-user, single-link regular file. Directory components and the leaf
773
+ are opened with ``O_NOFOLLOW`` and the leaf inode is re-attested around
774
+ every ledger operation.
775
+ """
776
+
777
+ def __init__(self, path: str | Path) -> None:
778
+ raw = os.path.expanduser(str(path))
779
+ candidate = Path(raw)
780
+ if (
781
+ not candidate.is_absolute()
782
+ or candidate != Path(os.path.normpath(raw))
783
+ or not candidate.name
784
+ or any(component in {".", ".."} for component in candidate.parts)
785
+ ):
786
+ raise PrivateGovernedActionLedgerError(
787
+ "governed action ledger path must be absolute and normalized"
788
+ )
789
+ required = ("O_DIRECTORY", "O_NOFOLLOW")
790
+ if any(not hasattr(os, name) for name in required) or not hasattr(
791
+ os, "geteuid"
792
+ ):
793
+ raise PrivateGovernedActionLedgerError(
794
+ "governed action ledger requires POSIX no-follow file access"
795
+ )
796
+ self.path = candidate
797
+ self.euid = os.geteuid()
798
+
799
+ @staticmethod
800
+ def _same_inode(left: os.stat_result, right: os.stat_result) -> bool:
801
+ return (left.st_dev, left.st_ino) == (right.st_dev, right.st_ino)
802
+
803
+ def _validate_directory(
804
+ self, value: os.stat_result, *, private_parent: bool
805
+ ) -> None:
806
+ if not stat.S_ISDIR(value.st_mode):
807
+ raise PrivateGovernedActionLedgerError(
808
+ "governed action ledger parent chain is not a directory"
809
+ )
810
+ if private_parent:
811
+ if value.st_uid != self.euid or stat.S_IMODE(value.st_mode) != 0o700:
812
+ raise PrivateGovernedActionLedgerError(
813
+ "governed action ledger parent must be current-user mode 0700"
814
+ )
815
+ elif value.st_uid not in {0, self.euid}:
816
+ raise PrivateGovernedActionLedgerError(
817
+ "governed action ledger ancestor has an untrusted owner"
818
+ )
819
+
820
+ def _open_parent(self) -> int:
821
+ flags = (
822
+ os.O_RDONLY
823
+ | os.O_DIRECTORY
824
+ | os.O_NOFOLLOW
825
+ | getattr(os, "O_CLOEXEC", 0)
826
+ )
827
+ try:
828
+ current_fd = os.open(self.path.anchor, flags)
829
+ except OSError:
830
+ raise PrivateGovernedActionLedgerError(
831
+ "governed action ledger root cannot be opened"
832
+ ) from None
833
+ components = self.path.parts[1:-1]
834
+ try:
835
+ self._validate_directory(
836
+ os.fstat(current_fd), private_parent=not components
837
+ )
838
+ for index, component in enumerate(components):
839
+ private_parent = index == len(components) - 1
840
+ try:
841
+ before = os.stat(
842
+ component, dir_fd=current_fd, follow_symlinks=False
843
+ )
844
+ except FileNotFoundError:
845
+ try:
846
+ os.mkdir(component, 0o700, dir_fd=current_fd)
847
+ except FileExistsError:
848
+ pass
849
+ except OSError:
850
+ raise PrivateGovernedActionLedgerError(
851
+ "governed action ledger parent cannot be created"
852
+ ) from None
853
+ try:
854
+ before = os.stat(
855
+ component, dir_fd=current_fd, follow_symlinks=False
856
+ )
857
+ except OSError:
858
+ raise PrivateGovernedActionLedgerError(
859
+ "governed action ledger parent is unstable"
860
+ ) from None
861
+ self._validate_directory(before, private_parent=private_parent)
862
+ try:
863
+ next_fd = os.open(component, flags, dir_fd=current_fd)
864
+ except OSError:
865
+ raise PrivateGovernedActionLedgerError(
866
+ "governed action ledger parent cannot be opened without links"
867
+ ) from None
868
+ try:
869
+ after = os.fstat(next_fd)
870
+ self._validate_directory(after, private_parent=private_parent)
871
+ if not self._same_inode(before, after):
872
+ raise PrivateGovernedActionLedgerError(
873
+ "governed action ledger parent changed during open"
874
+ )
875
+ except BaseException:
876
+ os.close(next_fd)
877
+ raise
878
+ os.close(current_fd)
879
+ current_fd = next_fd
880
+ return current_fd
881
+ except BaseException:
882
+ os.close(current_fd)
883
+ raise
884
+
885
+ def _validate_leaf(self, value: os.stat_result) -> None:
886
+ if (
887
+ not stat.S_ISREG(value.st_mode)
888
+ or value.st_uid != self.euid
889
+ or value.st_nlink != 1
890
+ or stat.S_IMODE(value.st_mode) != 0o600
891
+ ):
892
+ raise PrivateGovernedActionLedgerError(
893
+ "governed action ledger must be a current-user mode-0600 "
894
+ "single-link file"
895
+ )
896
+
897
+ def _assert_leaf(self, parent_fd: int, identity: tuple[int, int]) -> None:
898
+ try:
899
+ current = os.stat(
900
+ self.path.name, dir_fd=parent_fd, follow_symlinks=False
901
+ )
902
+ except OSError:
903
+ raise PrivateGovernedActionLedgerError(
904
+ "governed action ledger disappeared"
905
+ ) from None
906
+ self._validate_leaf(current)
907
+ if (current.st_dev, current.st_ino) != identity:
908
+ raise PrivateGovernedActionLedgerError(
909
+ "governed action ledger changed identity"
910
+ )
911
+
912
+ def _assert_no_wal_sidecars(self, parent_fd: int) -> None:
913
+ for suffix in ("-wal", "-shm"):
914
+ try:
915
+ os.stat(
916
+ self.path.name + suffix,
917
+ dir_fd=parent_fd,
918
+ follow_symlinks=False,
919
+ )
920
+ except FileNotFoundError:
921
+ continue
922
+ except OSError:
923
+ raise PrivateGovernedActionLedgerError(
924
+ "governed action ledger sidecar posture is unavailable"
925
+ ) from None
926
+ raise PrivateGovernedActionLedgerError(
927
+ "governed action ledger must not retain WAL or SHM sidecars"
928
+ )
929
+
930
+ def _open_leaf(self) -> tuple[int, int, tuple[int, int]]:
931
+ parent_fd = self._open_parent()
932
+ flags = os.O_RDWR | os.O_NOFOLLOW | getattr(os, "O_CLOEXEC", 0)
933
+ leaf_fd: int | None = None
934
+ try:
935
+ try:
936
+ before = os.stat(
937
+ self.path.name, dir_fd=parent_fd, follow_symlinks=False
938
+ )
939
+ self._validate_leaf(before)
940
+ leaf_fd = os.open(self.path.name, flags, dir_fd=parent_fd)
941
+ opened = os.fstat(leaf_fd)
942
+ self._validate_leaf(opened)
943
+ if not self._same_inode(before, opened):
944
+ raise PrivateGovernedActionLedgerError(
945
+ "governed action ledger changed during open"
946
+ )
947
+ except FileNotFoundError:
948
+ try:
949
+ leaf_fd = os.open(
950
+ self.path.name,
951
+ flags | os.O_CREAT | os.O_EXCL,
952
+ 0o600,
953
+ dir_fd=parent_fd,
954
+ )
955
+ except FileExistsError:
956
+ before = os.stat(
957
+ self.path.name, dir_fd=parent_fd, follow_symlinks=False
958
+ )
959
+ self._validate_leaf(before)
960
+ leaf_fd = os.open(self.path.name, flags, dir_fd=parent_fd)
961
+ opened = os.fstat(leaf_fd)
962
+ if not self._same_inode(before, opened):
963
+ raise PrivateGovernedActionLedgerError(
964
+ "governed action ledger changed during create"
965
+ )
966
+ else:
967
+ os.fchmod(leaf_fd, 0o600)
968
+ opened = os.fstat(leaf_fd)
969
+ self._validate_leaf(opened)
970
+ identity = (opened.st_dev, opened.st_ino)
971
+ self._assert_leaf(parent_fd, identity)
972
+ self._assert_no_wal_sidecars(parent_fd)
973
+ return parent_fd, leaf_fd, identity
974
+ except BaseException:
975
+ if leaf_fd is not None:
976
+ os.close(leaf_fd)
977
+ os.close(parent_fd)
978
+ raise
979
+
980
+ def connect(self) -> tuple[sqlite3.Connection, tuple[int, int]]:
981
+ parent_fd, leaf_fd, identity = self._open_leaf()
982
+ connection: sqlite3.Connection | None = None
983
+ try:
984
+ connection = sqlite3.connect(
985
+ str(self.path), check_same_thread=False, timeout=5.0
986
+ )
987
+ self._assert_leaf(parent_fd, identity)
988
+ mode = connection.execute("PRAGMA journal_mode=DELETE").fetchone()[0]
989
+ if str(mode).lower() != "delete":
990
+ raise PrivateGovernedActionLedgerError(
991
+ "governed action ledger could not disable persistent sidecars"
992
+ )
993
+ self._assert_no_wal_sidecars(parent_fd)
994
+ return connection, identity
995
+ except BaseException:
996
+ if connection is not None:
997
+ connection.close()
998
+ raise
999
+ finally:
1000
+ os.close(leaf_fd)
1001
+ os.close(parent_fd)
1002
+
1003
+ def assert_current(self, identity: tuple[int, int]) -> None:
1004
+ parent_fd = self._open_parent()
1005
+ try:
1006
+ self._assert_leaf(parent_fd, identity)
1007
+ self._assert_no_wal_sidecars(parent_fd)
1008
+ finally:
1009
+ os.close(parent_fd)
1010
+
1011
+
1012
+ class GovernedActionLedger:
1013
+ """SQLite truth for one-mutation-at-most governed effects."""
1014
+
1015
+ def __init__(self, db_path: str | Path, *, clock=time.time) -> None:
1016
+ self._storage = _PrivateLedgerPath(db_path)
1017
+ self.path = self._storage.path
1018
+ self.clock = clock
1019
+ self._lock = threading.RLock()
1020
+ self._conn, self._identity = self._storage.connect()
1021
+ self._conn.row_factory = sqlite3.Row
1022
+ self._conn.execute("PRAGMA synchronous=FULL")
1023
+ self._conn.execute("PRAGMA busy_timeout=5000")
1024
+ self._conn.execute(
1025
+ """
1026
+ CREATE TABLE IF NOT EXISTS governed_actions (
1027
+ action_id TEXT PRIMARY KEY,
1028
+ request_sha256 TEXT NOT NULL,
1029
+ execution_digest TEXT NOT NULL,
1030
+ request_json TEXT NOT NULL,
1031
+ owner_person_id TEXT NOT NULL,
1032
+ state TEXT NOT NULL CHECK (
1033
+ state IN ('prepared','executing','completed','failed','ambiguous')
1034
+ ),
1035
+ result_json TEXT,
1036
+ created_at REAL NOT NULL,
1037
+ updated_at REAL NOT NULL,
1038
+ started_at REAL
1039
+ )
1040
+ """
1041
+ )
1042
+ self._conn.execute(
1043
+ "CREATE UNIQUE INDEX IF NOT EXISTS governed_actions_execution_digest "
1044
+ "ON governed_actions(execution_digest)"
1045
+ )
1046
+ self._conn.commit()
1047
+ self._storage.assert_current(self._identity)
1048
+ self._recover_executing()
1049
+
1050
+ @staticmethod
1051
+ def _request_sha(request: Mapping[str, Any]) -> str:
1052
+ return sha256_json(dict(request))
1053
+
1054
+ def _recover_executing(self) -> None:
1055
+ now = _clock_value(self.clock)
1056
+ with self._lock:
1057
+ self._storage.assert_current(self._identity)
1058
+ rows = self._conn.execute(
1059
+ "SELECT action_id, request_json FROM governed_actions WHERE state = 'executing'"
1060
+ ).fetchall()
1061
+ if not rows:
1062
+ return
1063
+ self._conn.execute("BEGIN IMMEDIATE")
1064
+ try:
1065
+ for row in rows:
1066
+ request = json.loads(row["request_json"])
1067
+ result = _state_result(request, state="ambiguous", observed_at=now)
1068
+ self._conn.execute(
1069
+ "UPDATE governed_actions SET state = 'ambiguous', result_json = ?, "
1070
+ "updated_at = ? WHERE action_id = ? AND state = 'executing'",
1071
+ (canonical_json(result), now, row["action_id"]),
1072
+ )
1073
+ self._conn.commit()
1074
+ self._storage.assert_current(self._identity)
1075
+ except Exception:
1076
+ self._conn.rollback()
1077
+ self._storage.assert_current(self._identity)
1078
+ raise
1079
+
1080
+ def get(self, action_id: str) -> dict[str, Any] | None:
1081
+ with self._lock:
1082
+ self._storage.assert_current(self._identity)
1083
+ row = self._conn.execute(
1084
+ "SELECT * FROM governed_actions WHERE action_id = ?", (action_id,)
1085
+ ).fetchone()
1086
+ self._storage.assert_current(self._identity)
1087
+ if row is None:
1088
+ return None
1089
+ value = dict(row)
1090
+ value["request"] = json.loads(value.pop("request_json"))
1091
+ raw_result = value.pop("result_json")
1092
+ value["result"] = json.loads(raw_result) if raw_result is not None else None
1093
+ return value
1094
+
1095
+ def prepare_execution(
1096
+ self,
1097
+ request: Mapping[str, Any],
1098
+ *,
1099
+ owner_person_id: str,
1100
+ ) -> tuple[dict[str, Any], bool]:
1101
+ action_id = request["action_id"]
1102
+ request_json = canonical_json(dict(request))
1103
+ request_sha = hashlib.sha256(request_json.encode("utf-8")).hexdigest()
1104
+ now = _clock_value(self.clock)
1105
+ with self._lock:
1106
+ self._storage.assert_current(self._identity)
1107
+ self._conn.execute("BEGIN IMMEDIATE")
1108
+ try:
1109
+ row = self._conn.execute(
1110
+ "SELECT * FROM governed_actions WHERE action_id = ?", (action_id,)
1111
+ ).fetchone()
1112
+ created = row is None
1113
+ if row is None:
1114
+ try:
1115
+ self._conn.execute(
1116
+ """
1117
+ INSERT INTO governed_actions (
1118
+ action_id, request_sha256, execution_digest, request_json,
1119
+ owner_person_id, state, result_json, created_at, updated_at
1120
+ ) VALUES (?, ?, ?, ?, ?, 'prepared', NULL, ?, ?)
1121
+ """,
1122
+ (
1123
+ action_id,
1124
+ request_sha,
1125
+ request["execution_digest"],
1126
+ request_json,
1127
+ owner_person_id,
1128
+ now,
1129
+ now,
1130
+ ),
1131
+ )
1132
+ except sqlite3.IntegrityError as exc:
1133
+ raise GovernedActionConflict(
1134
+ "execution digest is already bound to another action"
1135
+ ) from exc
1136
+ else:
1137
+ if (
1138
+ row["request_sha256"] != request_sha
1139
+ or row["execution_digest"] != request["execution_digest"]
1140
+ or row["owner_person_id"] != owner_person_id
1141
+ ):
1142
+ raise GovernedActionConflict(
1143
+ "action_id is already bound to another execution"
1144
+ )
1145
+ self._conn.commit()
1146
+ self._storage.assert_current(self._identity)
1147
+ except Exception:
1148
+ self._conn.rollback()
1149
+ self._storage.assert_current(self._identity)
1150
+ raise
1151
+ record = self.get(action_id)
1152
+ assert record is not None
1153
+ return record, created
1154
+
1155
+ def mark_executing(self, action_id: str) -> dict[str, Any]:
1156
+ now = _clock_value(self.clock)
1157
+ with self._lock:
1158
+ self._storage.assert_current(self._identity)
1159
+ self._conn.execute("BEGIN IMMEDIATE")
1160
+ try:
1161
+ cursor = self._conn.execute(
1162
+ "UPDATE governed_actions SET state = 'executing', result_json = NULL, "
1163
+ "started_at = ?, updated_at = ? "
1164
+ "WHERE action_id = ? AND state = 'prepared'",
1165
+ (now, now, action_id),
1166
+ )
1167
+ if cursor.rowcount != 1:
1168
+ raise GovernedActionConflict("action is not prepared for execution")
1169
+ self._conn.commit()
1170
+ self._storage.assert_current(self._identity)
1171
+ except Exception:
1172
+ self._conn.rollback()
1173
+ self._storage.assert_current(self._identity)
1174
+ raise
1175
+ record = self.get(action_id)
1176
+ assert record is not None
1177
+ return record
1178
+
1179
+ def finish(
1180
+ self,
1181
+ action_id: str,
1182
+ *,
1183
+ state: str,
1184
+ effect: Mapping[str, Any] | None = None,
1185
+ ) -> dict[str, Any]:
1186
+ if state not in {"completed", "ambiguous"}:
1187
+ raise ValueError("executing action may only complete or become ambiguous")
1188
+ now = _clock_value(self.clock)
1189
+ record = self.get(action_id)
1190
+ if record is None or record["state"] != "executing":
1191
+ raise GovernedActionConflict("action is not executing")
1192
+ result = _state_result(
1193
+ record["request"], state=state, observed_at=now, effect=effect
1194
+ )
1195
+ encoded = canonical_json(result)
1196
+ with self._lock:
1197
+ self._storage.assert_current(self._identity)
1198
+ self._conn.execute("BEGIN IMMEDIATE")
1199
+ try:
1200
+ cursor = self._conn.execute(
1201
+ "UPDATE governed_actions SET state = ?, result_json = ?, updated_at = ? "
1202
+ "WHERE action_id = ? AND state = 'executing'",
1203
+ (state, encoded, now, action_id),
1204
+ )
1205
+ if cursor.rowcount != 1:
1206
+ raise GovernedActionConflict("action execution state changed")
1207
+ self._conn.commit()
1208
+ self._storage.assert_current(self._identity)
1209
+ except Exception:
1210
+ self._conn.rollback()
1211
+ self._storage.assert_current(self._identity)
1212
+ raise
1213
+ return result
1214
+
1215
+ def fail_prepared(self, action_id: str) -> dict[str, Any]:
1216
+ now = _clock_value(self.clock)
1217
+ record = self.get(action_id)
1218
+ if record is None or record["state"] != "prepared":
1219
+ raise GovernedActionConflict("action is not prepared")
1220
+ result = _state_result(record["request"], state="failed", observed_at=now)
1221
+ encoded = canonical_json(result)
1222
+ with self._lock:
1223
+ self._storage.assert_current(self._identity)
1224
+ self._conn.execute("BEGIN IMMEDIATE")
1225
+ try:
1226
+ cursor = self._conn.execute(
1227
+ "UPDATE governed_actions SET state = 'failed', result_json = ?, "
1228
+ "updated_at = ? WHERE action_id = ? AND state = 'prepared'",
1229
+ (encoded, now, action_id),
1230
+ )
1231
+ if cursor.rowcount != 1:
1232
+ raise GovernedActionConflict("action preparation state changed")
1233
+ self._conn.commit()
1234
+ self._storage.assert_current(self._identity)
1235
+ except Exception:
1236
+ self._conn.rollback()
1237
+ self._storage.assert_current(self._identity)
1238
+ raise
1239
+ return result
1240
+
1241
+ def close(self) -> None:
1242
+ with self._lock:
1243
+ self._conn.close()
1244
+
1245
+
1246
+ class GovernedActionService:
1247
+ """Validate, fence, execute once, and project one governed action."""
1248
+
1249
+ def __init__(self, ledger: GovernedActionLedger, executor, *, clock=time.time) -> None:
1250
+ if not isinstance(ledger, GovernedActionLedger):
1251
+ raise TypeError("governed action ledger is required")
1252
+ if not callable(getattr(executor, "prepare", None)) or not callable(
1253
+ getattr(executor, "perform", None)
1254
+ ):
1255
+ raise TypeError("governed action executor is invalid")
1256
+ self.ledger = ledger
1257
+ self.executor = executor
1258
+ self.clock = clock
1259
+ # Owner-approved mutations are rare. A single dispatch lock avoids a
1260
+ # growing per-action lock cache and makes same-process concurrency
1261
+ # obviously one-writer without affecting read-only observations.
1262
+ self._dispatch_lock = asyncio.Lock()
1263
+
1264
+ async def execute(
1265
+ self,
1266
+ path_action_id: str,
1267
+ raw: bytes,
1268
+ authority: RequestAuthority,
1269
+ ) -> dict[str, Any]:
1270
+ owner = _owner_from_authority(authority, required_scope="actions:execute")
1271
+ request = parse_execution_request(raw, path_action_id=path_action_id)
1272
+ execution_context = _freeze_json(request)
1273
+ async with self._dispatch_lock:
1274
+ existing = self.ledger.get(request["action_id"])
1275
+ if existing is not None:
1276
+ expected = sha256_json(request)
1277
+ if (
1278
+ existing["request_sha256"] != expected
1279
+ or existing["owner_person_id"] != owner
1280
+ ):
1281
+ raise GovernedActionConflict(
1282
+ "action_id is already bound to another execution"
1283
+ )
1284
+ if existing["state"] != "prepared":
1285
+ if existing["result"] is None:
1286
+ return _state_result(
1287
+ existing["request"],
1288
+ state=existing["state"],
1289
+ observed_at=_clock_value(self.clock),
1290
+ )
1291
+ return existing["result"]
1292
+ try:
1293
+ _assert_live_approval(request, _clock_value(self.clock))
1294
+ except GovernedActionValidationError:
1295
+ return self.ledger.fail_prepared(request["action_id"])
1296
+ else:
1297
+ _assert_live_approval(request, _clock_value(self.clock))
1298
+ existing, _created = self.ledger.prepare_execution(
1299
+ request, owner_person_id=owner
1300
+ )
1301
+
1302
+ try:
1303
+ await self.executor.prepare(execution_context, owner)
1304
+ except Exception as exc:
1305
+ logger.warning(
1306
+ "Governed action %s failed read-only preparation (type=%s)",
1307
+ request["action_id"], type(exc).__name__,
1308
+ )
1309
+ return self.ledger.fail_prepared(request["action_id"])
1310
+
1311
+ try:
1312
+ _assert_live_approval(request, _clock_value(self.clock))
1313
+ except GovernedActionValidationError:
1314
+ return self.ledger.fail_prepared(request["action_id"])
1315
+
1316
+ self.ledger.mark_executing(request["action_id"])
1317
+ try:
1318
+ effect = await self.executor.perform(
1319
+ execution_context, owner
1320
+ )
1321
+ effect = _validated_effect(effect)
1322
+ except BaseException as exc:
1323
+ # BaseException is deliberate: cancellation, shutdown, or any
1324
+ # other unwind after the durable executing marker has an
1325
+ # uncertain effect outcome and therefore must never retry.
1326
+ logger.warning(
1327
+ "Governed action %s has an ambiguous effect outcome (type=%s)",
1328
+ request["action_id"], type(exc).__name__,
1329
+ )
1330
+ ambiguous = self.ledger.finish(
1331
+ request["action_id"], state="ambiguous"
1332
+ )
1333
+ if isinstance(exc, (asyncio.CancelledError, KeyboardInterrupt, SystemExit)):
1334
+ raise
1335
+ return ambiguous
1336
+ return self.ledger.finish(
1337
+ request["action_id"], state="completed", effect=effect
1338
+ )
1339
+
1340
+ async def observe(
1341
+ self, action_id: str, authority: RequestAuthority
1342
+ ) -> dict[str, Any]:
1343
+ _owner_from_authority(authority, required_scope="actions:verify")
1344
+ action_id = _valid_action_id(action_id)
1345
+ record = self.ledger.get(action_id)
1346
+ if record is None:
1347
+ raise GovernedActionNotFound("governed action was not found")
1348
+ if record["owner_person_id"] != authority.viewer_person_id:
1349
+ raise PermissionError("governed action is outside the owner binding")
1350
+ if record["result"] is not None:
1351
+ return record["result"]
1352
+ return _state_result(
1353
+ record["request"],
1354
+ state=record["state"],
1355
+ observed_at=_clock_value(self.clock),
1356
+ )
1357
+
1358
+ def close(self) -> None:
1359
+ self.ledger.close()
1360
+
1361
+
1362
+ async def _await(value):
1363
+ return await value if inspect.isawaitable(value) else value
1364
+
1365
+
1366
+ class ColonySubsystemActionExecutor:
1367
+ """Map the ten generic action names onto existing Colony subsystems."""
1368
+
1369
+ def __init__(
1370
+ self,
1371
+ *,
1372
+ graph=None,
1373
+ goals=None,
1374
+ commitments=None,
1375
+ initiatives=None,
1376
+ projects=None,
1377
+ feedback=None,
1378
+ autonomy_enable=None,
1379
+ autonomy_disable=None,
1380
+ autonomy_running=None,
1381
+ ) -> None:
1382
+ self.graph = graph
1383
+ self.goals = goals
1384
+ self.commitments = commitments
1385
+ self.initiatives = initiatives
1386
+ self.projects = projects
1387
+ self.feedback = feedback
1388
+ self.autonomy_enable = autonomy_enable
1389
+ self.autonomy_disable = autonomy_disable
1390
+ self.autonomy_running = autonomy_running
1391
+
1392
+ def _dependency(self, value, name: str):
1393
+ if value is None:
1394
+ raise RuntimeError(f"{name} is not initialized")
1395
+ return value
1396
+
1397
+ @staticmethod
1398
+ def _governed_research_project(
1399
+ request: Mapping[str, Any], owner_person_id: str,
1400
+ ):
1401
+ from apsimo.projects import Project
1402
+ from apsimo.work_orders import action_authority
1403
+
1404
+ args = request["args"]
1405
+ approval = request["approval"]
1406
+ identity_material = {
1407
+ "schema": "ColonyGovernedResearchProjectIdentityV1",
1408
+ "version": 1,
1409
+ "owner_person_id": owner_person_id,
1410
+ "action_id": request["action_id"],
1411
+ "action_digest": request["action_digest"],
1412
+ "execution_digest": request["execution_digest"],
1413
+ }
1414
+ identity = sha256_json(identity_material)
1415
+ depth = args.get("depth", "quick")
1416
+ return Project(
1417
+ id="proj-governed-" + identity[:20],
1418
+ title="Governed research " + identity[:8],
1419
+ objective=(
1420
+ "Research topic:\n" + args["topic"] + "\nDepth: " + depth
1421
+ ),
1422
+ source="governed_action",
1423
+ status="planning",
1424
+ outcome="pending",
1425
+ entity_ids=[],
1426
+ reason="",
1427
+ replans=0,
1428
+ next_review_at=0.0,
1429
+ source_event_refs=[
1430
+ "governed-action:" + request["action_id"],
1431
+ "governed-intent:" + request["intent_id"],
1432
+ ],
1433
+ evidence_refs=[
1434
+ "action-digest:" + request["action_digest"],
1435
+ "intent-digest:" + request["intent_digest"],
1436
+ "args-digest:" + request["args_sha256"],
1437
+ "execution-digest:" + request["execution_digest"],
1438
+ ],
1439
+ policy_decision_refs=[
1440
+ "approval:" + approval["approval_id"],
1441
+ "decision:" + approval["decision_id"],
1442
+ "approval-revision:" + str(approval["revision"]),
1443
+ "authorization-receipt-digest:"
1444
+ + approval["authorization_receipt_sha256"],
1445
+ ],
1446
+ subject_person_id=owner_person_id,
1447
+ viewer_scope="owner",
1448
+ shareability="owner_private",
1449
+ capability_allowlist=list(action_authority("research")[1]),
1450
+ goal_fingerprint=identity,
1451
+ )
1452
+
1453
+ async def prepare(
1454
+ self, request: Mapping[str, Any], owner_person_id: str,
1455
+ ) -> None:
1456
+ tool_name = request["tool_name"]
1457
+ args = request["args"]
1458
+ if tool_name not in ACTION_TOOL_NAMES:
1459
+ raise RuntimeError("unknown governed Colony action")
1460
+ if tool_name == "colony_autonomy_enable":
1461
+ self._dependency(self.autonomy_enable, "autonomy control")
1462
+ self._dependency(self.autonomy_running, "autonomy status")
1463
+ elif tool_name == "colony_autonomy_disable":
1464
+ self._dependency(self.autonomy_disable, "autonomy control")
1465
+ self._dependency(self.autonomy_running, "autonomy status")
1466
+ elif tool_name == "colony_create_commitment":
1467
+ self._dependency(self.commitments, "commitment store")
1468
+ elif tool_name == "colony_resolve_commitment":
1469
+ store = self._dependency(self.commitments, "commitment store")
1470
+ row = store.get(args["commitment_id"])
1471
+ if row is None or row.get("person_id") != owner_person_id:
1472
+ raise RuntimeError("owner commitment was not found")
1473
+ elif tool_name in {
1474
+ "colony_task_complete",
1475
+ "colony_task_snooze",
1476
+ "colony_task_dismiss",
1477
+ }:
1478
+ goals = self._dependency(self.goals, "goal store")
1479
+ try:
1480
+ goals.get_goal(args["task_id"])
1481
+ except Exception as exc:
1482
+ raise RuntimeError("task was not found") from exc
1483
+ elif tool_name == "colony_initiative_feedback":
1484
+ initiatives = self._dependency(self.initiatives, "initiative store")
1485
+ if initiatives.get(args["initiative_id"]) is None:
1486
+ raise RuntimeError("initiative was not found")
1487
+ elif tool_name == "colony_record_insight":
1488
+ graph = self._dependency(self.graph, "memory graph")
1489
+ if not callable(getattr(graph, "store_memory", None)):
1490
+ raise RuntimeError("memory graph insight writer is not initialized")
1491
+ elif tool_name == "colony_research":
1492
+ projects = self._dependency(self.projects, "ProjectEngine")
1493
+ prepare = getattr(projects, "prepare_governed_research", None)
1494
+ enqueue = getattr(projects, "enqueue_governed_research", None)
1495
+ if not callable(prepare) or not callable(enqueue):
1496
+ raise RuntimeError(
1497
+ "ProjectEngine governed research handoff is not initialized"
1498
+ )
1499
+ prepare(self._governed_research_project(request, owner_person_id))
1500
+
1501
+ async def perform(
1502
+ self, request: Mapping[str, Any], owner_person_id: str
1503
+ ) -> dict[str, Any]:
1504
+ tool_name = request["tool_name"]
1505
+ args = request["args"]
1506
+ if tool_name == "colony_autonomy_enable":
1507
+ await _await(self.autonomy_enable())
1508
+ running = bool(await _await(self.autonomy_running()))
1509
+ return self._effect(
1510
+ "autonomy",
1511
+ "enabled" if running else "start_requested",
1512
+ {"running": running},
1513
+ )
1514
+ if tool_name == "colony_autonomy_disable":
1515
+ await _await(self.autonomy_disable())
1516
+ running = bool(await _await(self.autonomy_running()))
1517
+ return self._effect(
1518
+ "autonomy",
1519
+ "stop_requested" if running else "disabled",
1520
+ {"running": running},
1521
+ )
1522
+ if tool_name == "colony_create_commitment":
1523
+ row = self.commitments.create(
1524
+ person_id=owner_person_id,
1525
+ description=args["description"],
1526
+ due_at=args.get("due_at") or None,
1527
+ priority=args.get(
1528
+ "priority", GOVERNED_COMMITMENT_PRIORITY_DEFAULT,
1529
+ ),
1530
+ source_type="governed_action",
1531
+ source_context="owner_authorized",
1532
+ metadata={"governed": True},
1533
+ )
1534
+ effect_id = _effect_id("commitment", row.get("id"))
1535
+ return self._effect(
1536
+ effect_id,
1537
+ "created",
1538
+ {"commitment_id": effect_id, "status": _status_text(row.get("status"))},
1539
+ )
1540
+ if tool_name == "colony_resolve_commitment":
1541
+ row = self.commitments.resolve(
1542
+ args["commitment_id"],
1543
+ outcome=args.get("outcome", "done"),
1544
+ note=args.get("reason", ""),
1545
+ resolved_by="governed-owner",
1546
+ )
1547
+ if row is None:
1548
+ raise RuntimeError("commitment was not found")
1549
+ effect_id = _effect_id("commitment", row.get("id") or args["commitment_id"])
1550
+ return self._effect(
1551
+ effect_id,
1552
+ "resolved",
1553
+ {"commitment_id": effect_id, "status": _status_text(row.get("status"))},
1554
+ )
1555
+ if tool_name == "colony_initiative_feedback":
1556
+ item = self.initiatives.get(args["initiative_id"])
1557
+ status_map = {
1558
+ "acknowledged": "acknowledged",
1559
+ "dismissed": "cancelled",
1560
+ "snoozed": "pending",
1561
+ "actioned": "completed",
1562
+ }
1563
+ action = args["action"]
1564
+ updated = self.initiatives.update(
1565
+ args["initiative_id"], status=status_map[action]
1566
+ )
1567
+ if updated is None:
1568
+ raise RuntimeError("initiative update failed")
1569
+ try:
1570
+ self.initiatives.log_history(
1571
+ args["initiative_id"],
1572
+ action="governed_" + action,
1573
+ agent_id="governed-action-worker",
1574
+ details=json.loads(canonical_json(args.get("details", {}))),
1575
+ )
1576
+ except Exception:
1577
+ logger.warning("Governed initiative history could not be recorded", exc_info=True)
1578
+ if self.feedback is not None and getattr(item, "type", None):
1579
+ try:
1580
+ self.feedback.record(item.type, action)
1581
+ except Exception:
1582
+ logger.warning("Governed initiative feedback could not be recorded", exc_info=True)
1583
+ effect_id = _effect_id("initiative", args["initiative_id"])
1584
+ return self._effect(
1585
+ effect_id,
1586
+ action,
1587
+ {
1588
+ "initiative_id": effect_id,
1589
+ "action": action,
1590
+ "status": _status_text(getattr(updated, "status", status_map[action])),
1591
+ },
1592
+ )
1593
+ if tool_name == "colony_record_insight":
1594
+ content_hash = sha256_json({
1595
+ "schema": "ColonyGovernedInsightIdentityV1",
1596
+ "version": 1,
1597
+ "person_id": owner_person_id,
1598
+ "insight_type": args["insight_type"],
1599
+ "source": "governed_action",
1600
+ "content": args["content"],
1601
+ })
1602
+ insight_id = await self.graph.store_memory(
1603
+ content=args["content"],
1604
+ memory_type="semantic",
1605
+ entities=[],
1606
+ metadata={
1607
+ "governed": True,
1608
+ "insight_type": args["insight_type"],
1609
+ "source": "governed_action",
1610
+ },
1611
+ importance=args.get("confidence", 0.7),
1612
+ person_id=owner_person_id,
1613
+ source_type="inference",
1614
+ content_hash=content_hash,
1615
+ )
1616
+ effect_id = _effect_id("insight", insight_id)
1617
+ return self._effect(
1618
+ effect_id,
1619
+ "recorded",
1620
+ {"insight_id": effect_id, "insight_type": args["insight_type"]},
1621
+ )
1622
+ if tool_name == "colony_research":
1623
+ project = self.projects.enqueue_governed_research(
1624
+ self._governed_research_project(request, owner_person_id)
1625
+ )
1626
+ effect_id = _effect_id("project", project.id)
1627
+ return self._effect(
1628
+ effect_id,
1629
+ "queued",
1630
+ {
1631
+ "project_id": effect_id,
1632
+ "depth": args.get("depth", "quick"),
1633
+ "status": _status_text(project.status),
1634
+ },
1635
+ )
1636
+ if tool_name == "colony_task_complete":
1637
+ if not self.goals.complete_task(args["task_id"]):
1638
+ raise RuntimeError("task completion failed")
1639
+ effect_id = _effect_id("task", args["task_id"])
1640
+ return self._effect(
1641
+ effect_id,
1642
+ "completed",
1643
+ {"task_id": effect_id, "status": "completed"},
1644
+ )
1645
+ if tool_name == "colony_task_snooze":
1646
+ hours = args.get("hours", 24)
1647
+ if not self.goals.snooze_task(
1648
+ args["task_id"], hours, args.get("reason", "")
1649
+ ):
1650
+ raise RuntimeError("task snooze failed")
1651
+ effect_id = _effect_id("task", args["task_id"])
1652
+ return self._effect(
1653
+ effect_id,
1654
+ "snoozed",
1655
+ {"task_id": effect_id, "status": "snoozed", "hours": hours},
1656
+ )
1657
+ if tool_name == "colony_task_dismiss":
1658
+ reason = args.get("reason", "stale")
1659
+ if not self.goals.dismiss_task(args["task_id"], reason):
1660
+ raise RuntimeError("task dismissal failed")
1661
+ effect_id = _effect_id("task", args["task_id"])
1662
+ return self._effect(
1663
+ effect_id,
1664
+ "dismissed",
1665
+ {"task_id": effect_id, "status": "dismissed", "reason": reason},
1666
+ )
1667
+ raise RuntimeError("unknown governed Colony action")
1668
+
1669
+ @staticmethod
1670
+ def _effect(effect_id: str, outcome: str, verification: Mapping[str, Any]):
1671
+ return {
1672
+ "schema": "ColonyGovernedActionEffectV1",
1673
+ "version": 1,
1674
+ "effect_id": _effect_id("effect", effect_id),
1675
+ "outcome": outcome,
1676
+ "verification": dict(verification),
1677
+ }
1678
+
1679
+
1680
+ __all__ = (
1681
+ "ACTION_TOOL_NAMES",
1682
+ "ColonySubsystemActionExecutor",
1683
+ "GOVERNED_ACTION_PRINCIPAL",
1684
+ "GOVERNED_ACTION_REQUEST_MAX_BYTES",
1685
+ "GOVERNED_ACTION_SCOPES",
1686
+ "GOVERNED_COMMITMENT_DESCRIPTION_MAX_CHARS",
1687
+ "GOVERNED_COMMITMENT_DUE_AT_MAX_CHARS",
1688
+ "GOVERNED_COMMITMENT_PRIORITY_DEFAULT",
1689
+ "GOVERNED_DETAILS_INTEGER_MAX",
1690
+ "GOVERNED_DETAILS_KEY_MAX_CHARS",
1691
+ "GOVERNED_DETAILS_MAX_DEPTH",
1692
+ "GOVERNED_DETAILS_MAX_NODES",
1693
+ "GOVERNED_DETAILS_STRING_MAX_CHARS",
1694
+ "GOVERNED_FREEFORM_REASON_MAX_CHARS",
1695
+ "GOVERNED_IDENTIFIER_MAX_CHARS",
1696
+ "GOVERNED_IDENTIFIER_PATTERN",
1697
+ "GOVERNED_INSIGHT_CONTENT_MAX_CHARS",
1698
+ "GOVERNED_RESEARCH_TOPIC_MAX_CHARS",
1699
+ "GovernedActionConflict",
1700
+ "GovernedActionLedger",
1701
+ "GovernedActionNotFound",
1702
+ "GovernedActionService",
1703
+ "GovernedActionValidationError",
1704
+ "PrivateGovernedActionLedgerError",
1705
+ "canonical_json",
1706
+ "parse_execution_request",
1707
+ "sha256_json",
1708
+ )