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
apsimo/work_orders.py ADDED
@@ -0,0 +1,925 @@
1
+ """Canonical WorkOrderV1 bridge from durable projects to external executors.
2
+
3
+ Colony owns the goal/work ledger; it does not execute external effects. A
4
+ project step becomes one deterministic ``agent_action`` queue job containing
5
+ bounded references and success criteria. The host-side Action Plane decides
6
+ authorization and records real effect receipts.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import importlib
13
+ import inspect
14
+ import json
15
+ import logging
16
+ import os
17
+ import re
18
+ from dataclasses import asdict, dataclass
19
+ from datetime import datetime, timedelta, timezone
20
+ from typing import Any, Dict, Iterable, Mapping, Optional, Tuple
21
+
22
+ from apsimo.execution_results import (
23
+ EXTERNAL_EFFECT_CLASSES,
24
+ ExecutionResultError,
25
+ ExecutionResultV1,
26
+ ReceiptVerificationV1,
27
+ )
28
+ from apsimo.projects.store import ProjectStore
29
+ from apsimo.scope_bounds import VIEWER_SCOPE_MAX_CHARS
30
+ from apsimo.task_queue.models import Job, JobPriority, JobStatus, JobType
31
+ from apsimo.task_queue.models import JobCapabilityRequirement
32
+ from apsimo.task_queue.routing import (
33
+ ACTION_PLANE_ROUTE,
34
+ WORK_ORDER_ROUTE,
35
+ )
36
+
37
+
38
+ WORK_ORDER_VERSION = 1
39
+ WORK_ORDER_EXECUTOR_CAPABILITY = WORK_ORDER_ROUTE
40
+ logger = logging.getLogger(__name__)
41
+ _SECRET_ASSIGNMENT = re.compile(
42
+ r"(?i)\b(api[_-]?key|authorization|bearer|credential|password|secret|token)"
43
+ r"\s*[:=]\s*(?:bearer\s+)?[^\s,;]+"
44
+ )
45
+ _IMPORT_PATH = re.compile(
46
+ r"^[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*:[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*$"
47
+ )
48
+ _ACTION_KIND = {
49
+ "analyze": ("agent_project_analyze", ("memory:read", "reasoning"), "internal"),
50
+ "research": ("agent_project_research", ("memory:read", "web:read", "reasoning"), "read_only"),
51
+ "internal": ("agent_project_internal", ("memory:read", "reasoning"), "internal"),
52
+ "directed": (
53
+ "agent_project_directed",
54
+ (
55
+ "code:execute",
56
+ "filesystem:read",
57
+ "filesystem:write",
58
+ "git:write",
59
+ "memory:read",
60
+ "reasoning",
61
+ "terminal:execute",
62
+ "web:read",
63
+ ),
64
+ "mutation",
65
+ ),
66
+ "deliver": ("agent_project_deliver", ("messaging:send",), "disclosure"),
67
+ }
68
+
69
+
70
+ def action_authority(action_kind: str) -> Tuple[str, Tuple[str, ...], str]:
71
+ """Return the canonical WorkOrder action/capability/risk contract."""
72
+
73
+ action = _ACTION_KIND.get(str(action_kind or ""))
74
+ if action is None:
75
+ raise WorkOrderError("unsupported project action kind")
76
+ return action
77
+
78
+
79
+ class WorkOrderError(ValueError):
80
+ pass
81
+
82
+
83
+ class ReceiptVerifierConfigurationError(RuntimeError):
84
+ """A configured external-effect verifier could not be built safely."""
85
+
86
+
87
+ def load_receipt_verifier_from_env(environ: Optional[Mapping[str, str]] = None) -> Any:
88
+ """Construct the configured receipt verifier without host coupling.
89
+
90
+ The verifier remains an operator-selected plugin. Colony knows only a
91
+ ``module:attribute`` factory and a bounded JSON object of keyword
92
+ arguments; it does not import any particular host deployment by default. An
93
+ explicitly configured verifier fails closed on every import, parse,
94
+ construction, or interface error.
95
+ """
96
+
97
+ env = os.environ if environ is None else environ
98
+ spec = str(env.get("COLONY_WORK_ORDER_RECEIPT_VERIFIER", "") or "").strip()
99
+ if not spec:
100
+ return None
101
+ if len(spec) > 512 or not _IMPORT_PATH.fullmatch(spec):
102
+ raise ReceiptVerifierConfigurationError(
103
+ "COLONY_WORK_ORDER_RECEIPT_VERIFIER must be module:attribute"
104
+ )
105
+
106
+ raw_config = str(
107
+ env.get("COLONY_WORK_ORDER_RECEIPT_VERIFIER_CONFIG", "{}") or "{}"
108
+ )
109
+ if len(raw_config.encode("utf-8")) > 16 * 1024:
110
+ raise ReceiptVerifierConfigurationError("receipt verifier config is too large")
111
+ try:
112
+ config = json.loads(raw_config)
113
+ except (TypeError, ValueError) as exc:
114
+ raise ReceiptVerifierConfigurationError(
115
+ "receipt verifier config must be valid JSON"
116
+ ) from exc
117
+ if not isinstance(config, dict) or len(config) > 64:
118
+ raise ReceiptVerifierConfigurationError(
119
+ "receipt verifier config must be a JSON object with at most 64 fields"
120
+ )
121
+ if not all(isinstance(key, str) and key for key in config):
122
+ raise ReceiptVerifierConfigurationError(
123
+ "receipt verifier config keys must be non-empty strings"
124
+ )
125
+
126
+ module_name, attribute_path = spec.split(":", 1)
127
+ try:
128
+ target = importlib.import_module(module_name)
129
+ for name in attribute_path.split("."):
130
+ target = getattr(target, name)
131
+ except (ImportError, AttributeError) as exc:
132
+ raise ReceiptVerifierConfigurationError(
133
+ "configured receipt verifier could not be imported"
134
+ ) from exc
135
+ if not callable(target):
136
+ raise ReceiptVerifierConfigurationError(
137
+ "configured receipt verifier factory is not callable"
138
+ )
139
+ try:
140
+ verifier = target(**config)
141
+ except Exception as exc:
142
+ raise ReceiptVerifierConfigurationError(
143
+ "configured receipt verifier factory failed"
144
+ ) from exc
145
+ if not callable(getattr(verifier, "verify", None)) and not callable(verifier):
146
+ raise ReceiptVerifierConfigurationError(
147
+ "configured receipt verifier has no callable verify interface"
148
+ )
149
+ return verifier
150
+
151
+
152
+ def _bounded_text(value: object, name: str, maximum: int) -> str:
153
+ result = str(value or "").strip()
154
+ if not result or len(result) > maximum:
155
+ raise WorkOrderError(f"{name} must be 1..{maximum} characters")
156
+ return _SECRET_ASSIGNMENT.sub(lambda match: f"{match.group(1)}=[REDACTED]", result)
157
+
158
+
159
+ def _canonical(value: object) -> str:
160
+ return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
161
+
162
+
163
+ @dataclass(frozen=True)
164
+ class WorkOrderV1:
165
+ work_order_id: str
166
+ idempotency_key: str
167
+ project_id: str
168
+ step_id: str
169
+ step_ordinal: int
170
+ objective: str
171
+ success_criteria: Tuple[str, ...]
172
+ context_refs: Tuple[str, ...]
173
+ capability_allowlist: Tuple[str, ...]
174
+ risk_class: str
175
+ recipient_scope: str
176
+ max_runtime_seconds: int
177
+ max_attempts: int
178
+ issued_at: str
179
+ deadline: str
180
+ action_hint: str
181
+ work_order_digest: str
182
+ source: str = "project_engine"
183
+ schema: str = "WorkOrderV1"
184
+ version: int = WORK_ORDER_VERSION
185
+
186
+ @classmethod
187
+ def from_payload(cls, payload: Mapping[str, Any]) -> "WorkOrderV1":
188
+ """Reconstruct and verify every authority-bearing queue field.
189
+
190
+ A WorkOrder digest is useful only when the queue validates the object
191
+ that produced it. Treating ``schema=WorkOrderV1`` as a routing hint
192
+ would let a malformed row acquire the privileged Action Plane route.
193
+ """
194
+
195
+ if not isinstance(payload, Mapping):
196
+ raise WorkOrderError("WorkOrder payload must be an object")
197
+ if payload.get("schema") != "WorkOrderV1":
198
+ raise WorkOrderError("WorkOrder schema is invalid")
199
+ if payload.get("version") != WORK_ORDER_VERSION:
200
+ raise WorkOrderError("WorkOrder version is stale")
201
+ try:
202
+ order = cls(
203
+ work_order_id=str(payload["work_order_id"]),
204
+ idempotency_key=str(payload["idempotency_key"]),
205
+ project_id=str(payload["project_id"]),
206
+ step_id=str(payload["step_id"]),
207
+ step_ordinal=int(payload["step_ordinal"]),
208
+ objective=str(payload["objective"]),
209
+ success_criteria=tuple(payload["success_criteria"]),
210
+ context_refs=tuple(payload["context_refs"]),
211
+ capability_allowlist=tuple(payload["capability_allowlist"]),
212
+ risk_class=str(payload["risk_class"]),
213
+ recipient_scope=str(payload["recipient_scope"]),
214
+ max_runtime_seconds=int(payload["max_runtime_seconds"]),
215
+ max_attempts=int(payload["max_attempts"]),
216
+ issued_at=str(payload["issued_at"]),
217
+ deadline=str(payload["deadline"]),
218
+ action_hint=str(payload["action_hint"]),
219
+ work_order_digest=str(payload["work_order_digest"]),
220
+ source=str(payload["source"]),
221
+ schema=str(payload["schema"]),
222
+ version=int(payload["version"]),
223
+ )
224
+ except (KeyError, TypeError, ValueError) as exc:
225
+ missing = exc.args[0] if isinstance(exc, KeyError) else str(exc)
226
+ raise WorkOrderError(
227
+ f"invalid WorkOrder authority field: {missing}"
228
+ ) from exc
229
+ order.validate()
230
+ order.validate_payload(payload)
231
+ return order
232
+
233
+ @classmethod
234
+ def for_project_step(
235
+ cls,
236
+ project,
237
+ step,
238
+ *,
239
+ context_refs: Iterable[str] = (),
240
+ now: Optional[datetime] = None,
241
+ ) -> "WorkOrderV1":
242
+ action = action_authority(str(step.action_kind or ""))
243
+ action_hint, capabilities, risk_class = action
244
+ project_id = _bounded_text(project.id, "project_id", 128)
245
+ step_id = _bounded_text(step.id, "step_id", 128)
246
+ project_source = _bounded_text(project.source, "project_source", 64)
247
+ boundary_subjects = " ".join(
248
+ item for item in (
249
+ str(getattr(step, "boundary_subject", "") or "").strip(),
250
+ str(getattr(project, "subject_person_id", "") or "").strip(),
251
+ " ".join(
252
+ str(item) for item in (
253
+ getattr(project, "entity_ids", None) or ()
254
+ ) if str(item).strip()
255
+ ),
256
+ ) if item
257
+ )
258
+ objective = _bounded_text(
259
+ f"Project: {project.title}\nObjective: {project.objective}\n"
260
+ f"Current step: {step.description}\n"
261
+ f"Boundary subject: {boundary_subjects or 'project subject'}",
262
+ "objective",
263
+ 4000,
264
+ )
265
+ criteria = (
266
+ "Complete only the named project step",
267
+ "Return structured status and evidence receipts",
268
+ "Do not claim an external effect without independent evidence",
269
+ )
270
+ refs = tuple(sorted({
271
+ _bounded_text(item, "context_ref", 256) for item in context_refs if str(item).strip()
272
+ }))
273
+ # The deadline is authority-relevant, so its source must be stable
274
+ # across polling/restarts. A caller may override it for deterministic
275
+ # construction; otherwise the durable step creation time is used.
276
+ observed = now or datetime.fromtimestamp(
277
+ float(
278
+ getattr(step, "work_order_issued_at", 0.0)
279
+ or getattr(step, "created_at", 0.0)
280
+ or getattr(project, "created_at", 0.0)
281
+ ),
282
+ tz=timezone.utc,
283
+ )
284
+ if observed.tzinfo is None:
285
+ observed = observed.replace(tzinfo=timezone.utc)
286
+ runtime = 900 if step.action_kind == "research" else 600
287
+ if project_source in {"cognition_spine", "governed_action"}:
288
+ recipient_scope = _bounded_text(
289
+ getattr(project, "viewer_scope", "") or "owner",
290
+ "recipient_scope", VIEWER_SCOPE_MAX_CHARS,
291
+ )
292
+ else:
293
+ recipient_scope = (
294
+ "owner" if project.source == "owner" else "project:%s" % project_id
295
+ )
296
+ authority = {
297
+ "schema": "WorkOrderV1",
298
+ "version": WORK_ORDER_VERSION,
299
+ "source": project_source,
300
+ "project_id": project_id,
301
+ "step_id": step_id,
302
+ "step_ordinal": int(step.ordinal),
303
+ "objective": objective,
304
+ "success_criteria": list(criteria),
305
+ "context_refs": list(refs),
306
+ "capability_allowlist": list(capabilities),
307
+ "risk_class": risk_class,
308
+ "recipient_scope": recipient_scope,
309
+ "max_runtime_seconds": runtime,
310
+ "max_attempts": 2,
311
+ "issued_at": observed.isoformat(),
312
+ "deadline": (observed + timedelta(hours=24)).isoformat(),
313
+ "action_hint": action_hint,
314
+ }
315
+ digest = cls.authority_digest_from_payload(authority)
316
+ return cls(
317
+ work_order_id="work-%s" % digest[:24],
318
+ idempotency_key="project-step:%s" % digest,
319
+ project_id=project_id,
320
+ step_id=step_id,
321
+ step_ordinal=int(step.ordinal),
322
+ objective=objective,
323
+ success_criteria=criteria,
324
+ context_refs=refs,
325
+ capability_allowlist=tuple(capabilities),
326
+ risk_class=risk_class,
327
+ recipient_scope=str(authority["recipient_scope"]),
328
+ max_runtime_seconds=runtime,
329
+ max_attempts=2,
330
+ issued_at=str(authority["issued_at"]),
331
+ deadline=str(authority["deadline"]),
332
+ action_hint=action_hint,
333
+ work_order_digest=digest,
334
+ source=project_source,
335
+ )
336
+
337
+ @property
338
+ def effect_class(self) -> str:
339
+ """Normalize authority risk into the effect class used by receipts."""
340
+
341
+ return self.risk_class if self.risk_class in (
342
+ "read_only", "mutation", "disclosure"
343
+ ) else "none"
344
+
345
+ def authority_payload(self) -> Dict[str, Any]:
346
+ """Return every field that may change execution authority or budget."""
347
+
348
+ return {
349
+ "schema": self.schema,
350
+ "version": self.version,
351
+ "source": self.source,
352
+ "project_id": self.project_id,
353
+ "step_id": self.step_id,
354
+ "step_ordinal": self.step_ordinal,
355
+ "objective": self.objective,
356
+ "success_criteria": list(self.success_criteria),
357
+ "context_refs": list(self.context_refs),
358
+ "capability_allowlist": list(self.capability_allowlist),
359
+ "risk_class": self.risk_class,
360
+ "recipient_scope": self.recipient_scope,
361
+ "max_runtime_seconds": self.max_runtime_seconds,
362
+ "max_attempts": self.max_attempts,
363
+ "issued_at": self.issued_at,
364
+ "deadline": self.deadline,
365
+ "action_hint": self.action_hint,
366
+ }
367
+
368
+ @classmethod
369
+ def authority_digest_from_payload(cls, payload: Mapping[str, Any]) -> str:
370
+ fields = (
371
+ "schema", "version", "source", "project_id", "step_id",
372
+ "step_ordinal", "objective", "success_criteria", "context_refs",
373
+ "capability_allowlist", "risk_class", "recipient_scope",
374
+ "max_runtime_seconds", "max_attempts", "issued_at", "deadline",
375
+ "action_hint",
376
+ )
377
+ try:
378
+ authority = {name: payload[name] for name in fields}
379
+ except KeyError as exc:
380
+ raise WorkOrderError(f"missing authority field: {exc.args[0]}") from exc
381
+ return hashlib.sha256(_canonical(authority).encode("utf-8")).hexdigest()
382
+
383
+ def validate(self) -> None:
384
+ if self.schema != "WorkOrderV1" or self.version != WORK_ORDER_VERSION:
385
+ raise WorkOrderError("WorkOrder schema/version is invalid")
386
+ _bounded_text(self.project_id, "project_id", 128)
387
+ _bounded_text(self.step_id, "step_id", 128)
388
+ _bounded_text(self.source, "source", 64)
389
+ _bounded_text(self.objective, "objective", 4000)
390
+ _bounded_text(
391
+ self.recipient_scope, "recipient_scope", VIEWER_SCOPE_MAX_CHARS,
392
+ )
393
+ if self.step_ordinal < 0:
394
+ raise WorkOrderError("step_ordinal must be non-negative")
395
+ if not self.success_criteria or len(self.success_criteria) > 32:
396
+ raise WorkOrderError("success_criteria must contain 1..32 entries")
397
+ if len(self.context_refs) > 128:
398
+ raise WorkOrderError("context_refs exceeds 128 entries")
399
+ if self.max_runtime_seconds <= 0 or self.max_runtime_seconds > 86400:
400
+ raise WorkOrderError("max_runtime_seconds is outside 1..86400")
401
+ if self.max_attempts <= 0 or self.max_attempts > 10:
402
+ raise WorkOrderError("max_attempts is outside 1..10")
403
+ if not re.fullmatch(r"[0-9a-f]{64}", self.work_order_digest):
404
+ raise WorkOrderError("WorkOrder digest is invalid")
405
+ matching = [
406
+ authority for authority in _ACTION_KIND.values()
407
+ if authority[0] == self.action_hint
408
+ ]
409
+ if len(matching) != 1:
410
+ raise WorkOrderError("WorkOrder action hint is unsupported")
411
+ _hint, expected_capabilities, expected_risk = matching[0]
412
+ if tuple(self.capability_allowlist) != tuple(expected_capabilities):
413
+ raise WorkOrderError(
414
+ "WorkOrder capability allowlist does not match action authority"
415
+ )
416
+ if self.risk_class != expected_risk:
417
+ raise WorkOrderError(
418
+ "WorkOrder risk class does not match action authority"
419
+ )
420
+ for name, values, maximum in (
421
+ ("success_criteria", self.success_criteria, 1000),
422
+ ("context_refs", self.context_refs, 256),
423
+ ("capability_allowlist", self.capability_allowlist, 128),
424
+ ):
425
+ for value in values:
426
+ _bounded_text(value, name, maximum)
427
+ try:
428
+ issued = datetime.fromisoformat(self.issued_at.replace("Z", "+00:00"))
429
+ deadline = datetime.fromisoformat(self.deadline.replace("Z", "+00:00"))
430
+ except ValueError as exc:
431
+ raise WorkOrderError("WorkOrder timestamps must be ISO-8601") from exc
432
+ if issued.tzinfo is None or deadline.tzinfo is None or deadline <= issued:
433
+ raise WorkOrderError("WorkOrder deadline must follow issued_at")
434
+ digest = self.authority_digest_from_payload(self.authority_payload())
435
+ if digest != self.work_order_digest:
436
+ raise WorkOrderError("WorkOrder authority digest mismatch")
437
+ if self.work_order_id != "work-%s" % digest[:24]:
438
+ raise WorkOrderError("WorkOrder ID does not match authority digest")
439
+ if self.idempotency_key != "project-step:%s" % digest:
440
+ raise WorkOrderError("WorkOrder idempotency key does not match authority digest")
441
+
442
+ def legacy_work_order_id(self) -> str:
443
+ """Return the pre-P1 transport ID for migration collision detection.
444
+
445
+ It is never authorized for execution by the P1 adapter. Looking it up
446
+ before posting prevents an in-flight pre-P1 job from being duplicated
447
+ under the stronger digest.
448
+ """
449
+
450
+ legacy_identity = {
451
+ "version": self.version,
452
+ "project_id": self.project_id,
453
+ "step_id": self.step_id,
454
+ "step_ordinal": self.step_ordinal,
455
+ "objective": self.objective,
456
+ "context_refs": self.context_refs,
457
+ "action_hint": self.action_hint,
458
+ }
459
+ digest = hashlib.sha256(_canonical(legacy_identity).encode("utf-8")).hexdigest()
460
+ return "work-%s" % digest[:24]
461
+
462
+ def validate_payload(self, payload: Mapping[str, Any]) -> None:
463
+ digest = self.authority_digest_from_payload(payload)
464
+ if digest != self.work_order_digest:
465
+ raise WorkOrderError("queued WorkOrder authority digest mismatch")
466
+ if str(payload.get("work_order_digest") or "") != digest:
467
+ raise WorkOrderError("queued WorkOrder digest field mismatch")
468
+ if str(payload.get("work_order_id") or "") != self.work_order_id:
469
+ raise WorkOrderError("queued WorkOrder ID mismatch")
470
+ if int(payload.get("version") or 0) != self.version:
471
+ raise WorkOrderError("queued WorkOrder version is stale")
472
+
473
+ def payload(self) -> Dict[str, Any]:
474
+ self.validate()
475
+ result = asdict(self)
476
+ result["success_criteria"] = list(self.success_criteria)
477
+ result["context_refs"] = list(self.context_refs)
478
+ result["capability_allowlist"] = list(self.capability_allowlist)
479
+ # Compatibility fields used by the current host action adapter. They
480
+ # repeat bounded public metadata, never credentials or copied context.
481
+ result["description"] = self.objective
482
+ result["context"] = {
483
+ "WORK_ORDER_ID": self.work_order_id,
484
+ "PROJECT_ID": self.project_id,
485
+ "STEP_ID": self.step_id,
486
+ }
487
+ return result
488
+
489
+
490
+ class QueueWorkOrderAdapter:
491
+ """Idempotently post/poll WorkOrderV1 jobs on Colony's durable queue."""
492
+
493
+ def __init__(
494
+ self,
495
+ task_queue_manager,
496
+ *,
497
+ project_store: Optional[ProjectStore] = None,
498
+ receipt_verifier: Any = None,
499
+ approval_authority: Any = None,
500
+ posted_by: str = "project-engine",
501
+ ) -> None:
502
+ self.manager = task_queue_manager
503
+ # Production passes the durable ProjectEngine store. The in-memory
504
+ # default keeps direct/library construction backward compatible while
505
+ # preserving the same ledger invariants in tests.
506
+ self.project_store = project_store or ProjectStore()
507
+ self.receipt_verifier = receipt_verifier
508
+ self.approval_authority = approval_authority
509
+ self.posted_by = posted_by
510
+
511
+ def _approval_store(self):
512
+ if self.approval_authority is not None:
513
+ return self.approval_authority
514
+ from apsimo.initiatives.approval_authority import (
515
+ ApprovalAuthorityStore,
516
+ )
517
+
518
+ self.approval_authority = ApprovalAuthorityStore()
519
+ return self.approval_authority
520
+
521
+ async def _prepare_effect_authority(
522
+ self,
523
+ queue,
524
+ job: Job,
525
+ order: WorkOrderV1,
526
+ ) -> Job:
527
+ """Materialize or consume canonical authority while the job is blocked.
528
+
529
+ The queue row is born blocked, so a bounded grant can be consumed
530
+ without a claim race. Every retry reuses the exact grant-use or request
531
+ record and repairs an interrupted cross-database status transition.
532
+ """
533
+
534
+ from apsimo.initiatives.approval_authority import (
535
+ prepare_action_approval,
536
+ )
537
+
538
+ authority = prepare_action_approval(
539
+ self._approval_store(),
540
+ job_id=job.job_id,
541
+ job_type=job.job_type.value,
542
+ payload=job.payload,
543
+ deadline=job.deadline or order.deadline,
544
+ approval_started_at=job.posted_at,
545
+ )
546
+ state = authority["state"]
547
+ tags = authority["tags"]
548
+ target = None
549
+ reason = "approval_request_materialized"
550
+ if state in {"authorized_grant", "authorized_direct"}:
551
+ target = JobStatus.QUEUED
552
+ reason = (
553
+ "bounded_grant"
554
+ if state == "authorized_grant"
555
+ else "approval_decision_reconciled"
556
+ )
557
+ elif state == "rejected":
558
+ target = JobStatus.CANCELLED
559
+ reason = "approval_rejection_reconciled"
560
+ elif state in {"expired", "superseded"}:
561
+ target = JobStatus.FAILED
562
+ reason = "canonical_approval_%s" % state
563
+
564
+ if target is None:
565
+ await queue.merge_job_tags(job.job_id, tags)
566
+ else:
567
+ changed = await queue.update_job_status(
568
+ job.job_id,
569
+ target,
570
+ reason=reason,
571
+ tags=tags,
572
+ remove_tags=(
573
+ "hold_kind", "blocked_reason", "awaiting_owner_approval",
574
+ ),
575
+ )
576
+ if not changed:
577
+ raise WorkOrderError(
578
+ "canonical approval resolved but queue transition must reconcile"
579
+ )
580
+ refreshed = await queue.get_job(job.job_id)
581
+ return refreshed or job
582
+
583
+ async def execute(self, project, step, *, context_refs: Iterable[str] = ()) -> Tuple[Optional[bool], str]:
584
+ if not step.work_order_issued_at:
585
+ step.work_order_issued_at = datetime.now(timezone.utc).timestamp()
586
+ issued_at = datetime.fromtimestamp(step.work_order_issued_at, tz=timezone.utc)
587
+ order = WorkOrderV1.for_project_step(
588
+ project, step, context_refs=context_refs, now=issued_at
589
+ )
590
+ try:
591
+ if step.work_order_digest and step.work_order_digest != order.work_order_digest:
592
+ raise WorkOrderError(
593
+ "step authority changed after WorkOrder issue; explicit reissue required"
594
+ )
595
+ # Atomically insert missing parents or validate existing ones.
596
+ # Never let a stale polling object upsert a terminal lifecycle
597
+ # back to active before posting new work.
598
+ work_order_ref = self.project_store.prepare_work_order(
599
+ project, step, order,
600
+ )
601
+ step.work_order_ref = work_order_ref
602
+ step.work_order_digest = order.work_order_digest
603
+ except (ValueError, WorkOrderError) as exc:
604
+ return False, f"work order rejected: {exc}"
605
+
606
+ queue = self.manager.queue
607
+ job = await queue.get_job(order.work_order_id)
608
+ if job is None:
609
+ legacy_id = order.legacy_work_order_id()
610
+ legacy_job = await queue.get_job(legacy_id)
611
+ if legacy_job is not None:
612
+ return None, (
613
+ f"work_order:{legacy_id}:legacy_migration_hold; "
614
+ "cancel or reconcile it before canonical reissue"
615
+ )
616
+ job = Job(
617
+ job_id=order.work_order_id,
618
+ job_type=JobType.AGENT_ACTION,
619
+ payload=order.payload(),
620
+ priority=(JobPriority.HIGH if project.source == "owner" else JobPriority.NORMAL),
621
+ deadline=datetime.fromisoformat(order.deadline),
622
+ max_retries=max(0, order.max_attempts - 1),
623
+ timeout_secs=float(order.max_runtime_seconds),
624
+ posted_by=self.posted_by,
625
+ capabilities=[
626
+ JobCapabilityRequirement(name=capability)
627
+ for capability in (
628
+ WORK_ORDER_EXECUTOR_CAPABILITY,
629
+ ACTION_PLANE_ROUTE,
630
+ *order.capability_allowlist,
631
+ )
632
+ ],
633
+ tags={
634
+ "schema": order.schema,
635
+ "project_id": order.project_id,
636
+ "step_id": order.step_id,
637
+ "risk_class": order.risk_class,
638
+ "idempotency_key": order.idempotency_key,
639
+ "work_order_digest": order.work_order_digest,
640
+ "work_order_version": str(order.version),
641
+ "executor_protocol": WORK_ORDER_EXECUTOR_CAPABILITY,
642
+ },
643
+ )
644
+ if order.effect_class in EXTERNAL_EFFECT_CLASSES:
645
+ job.status = JobStatus.BLOCKED
646
+ job.tags.update({
647
+ "hold_kind": "approval",
648
+ "blocked_reason": "awaiting_owner_approval",
649
+ "awaiting_owner_approval": "true",
650
+ "approval_requested_at": datetime.now(
651
+ timezone.utc
652
+ ).isoformat(),
653
+ })
654
+ await queue.post(job)
655
+ if (
656
+ order.effect_class in EXTERNAL_EFFECT_CLASSES
657
+ and job.status is JobStatus.BLOCKED
658
+ and job.tags.get("blocked_reason") == "awaiting_owner_approval"
659
+ and not job.tags.get("approval_request_id")
660
+ ):
661
+ job = await self._prepare_effect_authority(
662
+ queue, job, order,
663
+ )
664
+ return None, (
665
+ f"work_order:{order.work_order_id}:"
666
+ f"{job.status.value}"
667
+ )
668
+
669
+ if (
670
+ order.effect_class in EXTERNAL_EFFECT_CLASSES
671
+ and job.status is JobStatus.BLOCKED
672
+ and job.tags.get("blocked_reason") == "awaiting_owner_approval"
673
+ ):
674
+ job = await self._prepare_effect_authority(queue, job, order)
675
+
676
+ try:
677
+ order.validate_payload(job.payload)
678
+ except WorkOrderError as exc:
679
+ return False, f"work order rejected: {exc}"
680
+
681
+ reconciliation = str(
682
+ job.tags.get("effect_reconciliation_finding") or ""
683
+ ).lower()
684
+ if reconciliation == "applied":
685
+ return None, (
686
+ f"work_order:{order.work_order_id}:"
687
+ "independently_verified_applied_no_retry"
688
+ )
689
+ if reconciliation == "not_applied":
690
+ return None, (
691
+ f"work_order:{order.work_order_id}:"
692
+ "independently_verified_not_applied_retry_available"
693
+ )
694
+
695
+ if job.status in (
696
+ JobStatus.QUEUED,
697
+ JobStatus.CLAIMED,
698
+ JobStatus.RUNNING,
699
+ JobStatus.BLOCKED,
700
+ JobStatus.ABANDONED,
701
+ ):
702
+ return None, f"work_order:{order.work_order_id}:{job.status.value}"
703
+ if job.status not in {JobStatus.COMPLETED, JobStatus.NEUTRAL}:
704
+ if job.status not in (JobStatus.CANCELLED, JobStatus.FAILED):
705
+ return False, f"work order {order.work_order_id} has unknown state {job.status.value}"
706
+
707
+ return await self._project_terminal_result(order, job, step=step)
708
+
709
+ async def reconcile_terminal_results(
710
+ self, *, limit: int = 25,
711
+ ) -> Dict[str, Any]:
712
+ """Project already-terminal queue truth without pursuing Projects.
713
+
714
+ This path never posts, claims, retries, or authorizes work. It only
715
+ polls immutable WorkOrders that already exist in the Project ledger
716
+ and materializes a terminal queue result through the same validation,
717
+ verification, and idempotent storage path used by ``execute``.
718
+ """
719
+
720
+ report: Dict[str, Any] = {
721
+ "checked": 0,
722
+ "terminal": 0,
723
+ "projected": 0,
724
+ "errors": 0,
725
+ }
726
+ queue = self.manager.queue
727
+ rows = self.project_store.work_orders_awaiting_reconciliation(
728
+ limit=limit,
729
+ )
730
+ terminal_states = {
731
+ JobStatus.COMPLETED,
732
+ JobStatus.NEUTRAL,
733
+ JobStatus.CANCELLED,
734
+ JobStatus.FAILED,
735
+ }
736
+ for row in rows:
737
+ report["checked"] += 1
738
+ work_order_id = str(row.get("work_order_id") or "")
739
+ try:
740
+ order = WorkOrderV1.from_payload(row["payload"])
741
+ if (
742
+ order.work_order_id != work_order_id
743
+ or order.work_order_digest != row["work_order_digest"]
744
+ or order.project_id != row["project_id"]
745
+ or order.step_id != row["step_id"]
746
+ ):
747
+ raise WorkOrderError(
748
+ "persisted WorkOrder ledger identity mismatch"
749
+ )
750
+ job = await queue.get_job(order.work_order_id)
751
+ if job is None or job.status not in terminal_states:
752
+ continue
753
+ report["terminal"] += 1
754
+ order.validate_payload(job.payload)
755
+ attempts_before = len(
756
+ self.project_store.execution_attempts_for(
757
+ order.work_order_id,
758
+ )
759
+ )
760
+ await self._project_terminal_result(order, job)
761
+ attempts_after = len(
762
+ self.project_store.execution_attempts_for(
763
+ order.work_order_id,
764
+ )
765
+ )
766
+ if attempts_after > attempts_before:
767
+ report["projected"] += attempts_after - attempts_before
768
+ except Exception as exc:
769
+ report["errors"] += 1
770
+ logger.warning(
771
+ "terminal WorkOrder reconciliation failed for %s: %s",
772
+ work_order_id[:64], exc,
773
+ )
774
+ return report
775
+
776
+ async def _project_terminal_result(
777
+ self, order: WorkOrderV1, job: Job, *, step: Any = None,
778
+ ) -> Tuple[Optional[bool], str]:
779
+ """Validate and persist one terminal queue outcome idempotently."""
780
+
781
+ queue = self.manager.queue
782
+ reconciliation = str(
783
+ job.tags.get("effect_reconciliation_finding") or ""
784
+ ).lower()
785
+ if reconciliation == "applied":
786
+ return None, (
787
+ f"work_order:{order.work_order_id}:"
788
+ "independently_verified_applied_no_retry"
789
+ )
790
+ if reconciliation == "not_applied":
791
+ return None, (
792
+ f"work_order:{order.work_order_id}:"
793
+ "independently_verified_not_applied_retry_available"
794
+ )
795
+
796
+ # A started mutation/disclosure that stopped without independent
797
+ # not-applied evidence is neither success nor failure. Keep the
798
+ # durable queue attempt/audit as reconciliation evidence, but never
799
+ # turn ambiguity into a false failed ExecutionResult or a replan that
800
+ # could duplicate the external effect.
801
+ if (
802
+ job.status is JobStatus.NEUTRAL
803
+ and str(job.tags.get("ambiguous_prior_effects") or "").lower()
804
+ == "true"
805
+ and str(job.tags.get("verification_pending") or "").lower()
806
+ == "true"
807
+ ):
808
+ return None, (
809
+ f"work_order:{order.work_order_id}:"
810
+ "awaiting_independent_effect_reconciliation"
811
+ )
812
+
813
+ try:
814
+ result = ExecutionResultV1.from_job(order, job)
815
+ result = await self._verify(order, result, job)
816
+ if (
817
+ result.terminal_outcome == "succeeded"
818
+ and result.verification_result == "verified"
819
+ ):
820
+ attested = await queue.attest_job_success(
821
+ order.work_order_id,
822
+ report={
823
+ "status": "verified",
824
+ "execution_result": result.payload(),
825
+ },
826
+ verifier_identity=result.verifier_identity,
827
+ verifier_type=str(
828
+ getattr(
829
+ self.receipt_verifier,
830
+ "verifier_type",
831
+ "receipt_verifier",
832
+ )
833
+ ),
834
+ )
835
+ if not attested:
836
+ return False, (
837
+ "independent execution result could not attest queue job"
838
+ )
839
+ transport_status = job.status.value
840
+ payload = result.payload()
841
+ prior_attempts = self.project_store.execution_attempts_for(
842
+ order.work_order_id,
843
+ )
844
+ exact_replay = any(
845
+ attempt["run_id"] == result.run_id
846
+ and int(attempt["attempt_number"]) == result.attempt_number
847
+ and attempt["transport_status"] == transport_status
848
+ and attempt["result"] == payload
849
+ for attempt in prior_attempts
850
+ )
851
+ if exact_replay:
852
+ # Avoid rewriting Step timestamps/outbox state every autonomy
853
+ # tick while retaining strict collision checks below for a
854
+ # reused run/attempt with different content.
855
+ result_ref = result.result_ref
856
+ else:
857
+ result_ref = self.project_store.save_execution_result(
858
+ order,
859
+ result,
860
+ transport_status=transport_status,
861
+ )
862
+ if step is not None:
863
+ # Preserve the existing execute() contract for its caller's
864
+ # in-memory Step; the store already bound the durable row.
865
+ step.result_ref = result_ref
866
+ except (ExecutionResultError, ValueError) as exc:
867
+ return False, f"execution result rejected: {exc}"
868
+
869
+ if result.terminal_outcome in ("skipped", "cancelled"):
870
+ reason = result.error or result.summary or result.terminal_outcome
871
+ return True, f"SKIPPED: {reason[:1200]}"
872
+ if result.terminal_outcome == "failed":
873
+ error = result.error or result.summary or "worker failed"
874
+ return False, f"work order {order.work_order_id} failed: {error[:1200]}"
875
+ if result.verification_result != "verified":
876
+ return None, (
877
+ f"work order {order.work_order_id} completed without verified evidence "
878
+ f"(result_ref={result.result_ref})"
879
+ )
880
+ summary = result.summary or "receipt-backed completion"
881
+ return True, f"work order {order.work_order_id} completed: {summary[:1200]}"
882
+
883
+ async def _verify(self, order: WorkOrderV1, result: ExecutionResultV1, job: Job) -> ExecutionResultV1:
884
+ if result.terminal_outcome != "succeeded":
885
+ return result.with_verification("not_applicable")
886
+
887
+ if self.receipt_verifier is None:
888
+ return result.with_verification(
889
+ "unverified",
890
+ error=result.error or (
891
+ "completion has no independent evidence verifier"
892
+ ),
893
+ )
894
+ verify = getattr(self.receipt_verifier, "verify", self.receipt_verifier)
895
+ decision = verify(
896
+ order=order,
897
+ result=result,
898
+ job=job,
899
+ output=dict(job.result.output or {}) if job.result else {},
900
+ )
901
+ if inspect.isawaitable(decision):
902
+ decision = await decision
903
+ checked = ReceiptVerificationV1.from_value(decision)
904
+ if (
905
+ checked.verified
906
+ and checked.verifier_identity == result.executor_identity
907
+ ):
908
+ return result.with_verification(
909
+ "unverified",
910
+ receipt_refs=checked.receipt_refs or result.receipt_refs,
911
+ verifier_identity=checked.verifier_identity,
912
+ error="executor cannot independently verify its own result",
913
+ )
914
+ if not checked.verified:
915
+ return result.with_verification(
916
+ "unverified",
917
+ receipt_refs=checked.receipt_refs or result.receipt_refs,
918
+ verifier_identity=checked.verifier_identity,
919
+ error=checked.detail or result.error or "external receipt verification failed",
920
+ )
921
+ return result.with_verification(
922
+ "verified",
923
+ receipt_refs=checked.receipt_refs,
924
+ verifier_identity=checked.verifier_identity,
925
+ )