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,1031 @@
1
+ """WorkerGovernor -- server-side enforcement for the distributed job queue
2
+ (cognition program, Phase B item 5).
3
+
4
+ The worker daemon is UNTRUSTED. Capability coverage, boundary compliance, and
5
+ the scope of what a worker actually did are all re-decided HERE, server-side,
6
+ never taken on the worker's word:
7
+
8
+ claim -> evaluate_claim(): re-verify (a) the worker's advertised
9
+ capabilities really cover the job's requirement and (b) the
10
+ job's subject is not under a standing owner boundary
11
+ (DirectiveGuard, capability-aware). A boundaried or
12
+ uncovered claim is refused server-side.
13
+ completion -> audit_report(): cross-check the worker's structured report
14
+ against what the job was authorized to do. A worker that
15
+ reports a mutation on a read-only job (or a force-push, or
16
+ out-of-scope deletes) is a VIOLATION, flagged loudly.
17
+ outcome -> record_outcome(): the audited verdict is the earned-autonomy
18
+ evidence. Each worker JOB TYPE is its own trust domain
19
+ ("worker:<job_type>") feeding the self-model/trust engine
20
+ (item 4): clean real completions graduate it, a violation
21
+ trips its circuit breaker. Feedback + journal + skill
22
+ distillation ride the same chokepoint.
23
+
24
+ Mode (COLONY_WORKERS_MODE, default shadow):
25
+ off -> governor disabled; the queue behaves exactly as before.
26
+ shadow -> CALIBRATION. Every claim is evaluated and journaled but ALLOWED
27
+ regardless (would_refuse is recorded, not enforced); only
28
+ explicitly verified completions become shadow competence events.
29
+ Neutral skips/unknowns never graduate a job type. Non-blocking by
30
+ design, so turning it on never disturbs the agent_action path.
31
+ live -> ENFORCING. A refused claim is blocked server-side; outcomes count
32
+ as real evidence toward earned autonomy.
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ from dataclasses import dataclass
38
+ import hashlib
39
+ import logging
40
+ import os
41
+ from typing import Any, Dict, Iterable, List, Optional
42
+
43
+ logger = logging.getLogger(__name__)
44
+
45
+ # Reported operations that imply the worker CHANGED something. A worker that
46
+ # reports any of these on a read-only job exceeded its grant.
47
+ _MUTATE_OPS = frozenset({
48
+ "modify_files", "write", "commit", "push", "push_branch", "open_pr",
49
+ "delete", "rm", "execute", "deploy", "send", "post", "create", "update",
50
+ })
51
+
52
+ # Risk tiers (from the action registry) that authorize a job to change state.
53
+ # A job at read_only risk that reports a mutation is a violation.
54
+ _MUTATING_RISK = frozenset({"low", "medium", "high", "outbound", "destructive"})
55
+
56
+
57
+ def job_declares_effect(job: Any) -> bool:
58
+ """Classify effectful work without treating its label as permission."""
59
+
60
+ payload = getattr(job, "payload", {}) or {}
61
+ risk = str(
62
+ payload.get("risk_class") or payload.get("risk") or ""
63
+ ).strip().lower()
64
+ action = str(
65
+ payload.get("action") or payload.get("command")
66
+ or payload.get("action_hint") or ""
67
+ ).strip().lower()
68
+ registered_action_effect = False
69
+ action_hint = str(payload.get("action_hint") or "").strip()
70
+ if action_hint:
71
+ try:
72
+ from apsimo.initiatives.action_registry import (
73
+ RiskTier,
74
+ get_action,
75
+ )
76
+
77
+ spec = get_action(action_hint)
78
+ registered_action_effect = bool(
79
+ spec is not None and spec.risk is not RiskTier.READ_ONLY
80
+ )
81
+ except Exception:
82
+ # Registry availability is not needed to preserve the other
83
+ # conservative effect signals below.
84
+ registered_action_effect = False
85
+ job_type = getattr(job, "job_type", "")
86
+ job_type = (
87
+ job_type.value if hasattr(job_type, "value") else str(job_type)
88
+ ).strip().lower()
89
+ required: set[str] = set()
90
+ try:
91
+ required.update(str(item) for item in job.required_capabilities())
92
+ except Exception:
93
+ pass
94
+ extra = str((getattr(job, "tags", {}) or {}).get(
95
+ "required_capability", ""
96
+ )).strip()
97
+ if extra:
98
+ required.add(extra)
99
+ effect_capability = any(
100
+ capability in {
101
+ "agent:tools", "code:execute", "filesystem:write",
102
+ "git:write", "messaging:send", "shell", "terminal:execute",
103
+ }
104
+ or capability.endswith(":write")
105
+ or capability.endswith(":send")
106
+ for capability in required
107
+ )
108
+ return bool(
109
+ payload.get("destructive") is True
110
+ or registered_action_effect
111
+ or effect_capability
112
+ or risk in (_MUTATING_RISK | {"mutation", "disclosure"})
113
+ or job_type == "system_maintenance"
114
+ or action in (_MUTATE_OPS | {
115
+ "disk_cleanup", "log_rotate", "db_vacuum",
116
+ "agent_project_directed", "agent_project_deliver",
117
+ })
118
+ )
119
+
120
+
121
+ def workers_mode() -> str:
122
+ from apsimo.util.autonomy_preset import resolve
123
+ explicit = os.environ.get("COLONY_WORKERS_MODE")
124
+ if explicit is not None and explicit.strip():
125
+ value = explicit.strip().lower()
126
+ if value not in {"off", "shadow", "live"}:
127
+ raise RuntimeError(
128
+ "COLONY_WORKERS_MODE must be off, shadow, or live"
129
+ )
130
+ return resolve("COLONY_WORKERS_MODE", ("off", "shadow", "live"), "shadow")
131
+
132
+
133
+ @dataclass(frozen=True)
134
+ class ClaimVerdict:
135
+ """Immutable authority result consumed by the central queue claim path.
136
+
137
+ Compatibility mapping helpers keep the existing read-only tests and API
138
+ serialization ergonomic, but callers cannot fabricate or mutate a partial
139
+ dictionary. Construction rejects inconsistent mode/boolean combinations.
140
+ """
141
+
142
+ mode: str
143
+ allowed: bool
144
+ enforced: bool
145
+ would_refuse: bool
146
+ reason: str
147
+ capability_ok: Optional[bool]
148
+ boundary_ok: Optional[bool]
149
+ boundary_reason: str
150
+ missing_capabilities: tuple[str, ...] = ()
151
+ shadow: bool = False
152
+ trust_ok: Optional[bool] = True
153
+ trust_reason: str = "trust_not_required"
154
+
155
+ def __post_init__(self) -> None:
156
+ if self.mode not in {"off", "shadow", "live"}:
157
+ raise ValueError("invalid worker-governor mode")
158
+ for field_name in ("allowed", "enforced", "would_refuse", "shadow"):
159
+ if type(getattr(self, field_name)) is not bool:
160
+ raise TypeError(f"{field_name} must be an exact boolean")
161
+ for field_name in ("capability_ok", "boundary_ok", "trust_ok"):
162
+ value = getattr(self, field_name)
163
+ if self.mode == "off":
164
+ # Mode off checks NOTHING: the verdict must say "unchecked"
165
+ # (None), never fabricate a pass — and never a bool at all,
166
+ # so an off verdict cannot be mistaken for an evaluated one.
167
+ if value is not None:
168
+ raise TypeError(
169
+ f"{field_name} must be None (unchecked) in mode 'off'"
170
+ )
171
+ elif type(value) is not bool:
172
+ raise TypeError(f"{field_name} must be an exact boolean")
173
+ if not isinstance(self.reason, str) or not isinstance(
174
+ self.boundary_reason, str
175
+ ):
176
+ raise TypeError("claim verdict reasons must be strings")
177
+ if not isinstance(self.missing_capabilities, tuple) or any(
178
+ not isinstance(item, str) for item in self.missing_capabilities
179
+ ):
180
+ raise TypeError("missing_capabilities must be a tuple of strings")
181
+
182
+ expected_refusal = not (
183
+ self.capability_ok and self.boundary_ok and self.trust_ok
184
+ )
185
+ if self.mode == "off":
186
+ # capability/boundary/trust are already required to be None
187
+ # ("unchecked") above.
188
+ valid = (
189
+ self.allowed and not self.enforced and not self.would_refuse
190
+ and not self.shadow
191
+ )
192
+ elif self.mode == "shadow":
193
+ valid = (
194
+ self.shadow
195
+ and self.enforced == (not self.allowed)
196
+ and self.would_refuse == expected_refusal
197
+ )
198
+ else:
199
+ valid = (
200
+ self.enforced and not self.shadow
201
+ and self.would_refuse == expected_refusal
202
+ and self.allowed == (not expected_refusal)
203
+ )
204
+ if not valid:
205
+ raise ValueError("inconsistent worker claim verdict")
206
+
207
+ def as_dict(self) -> Dict[str, Any]:
208
+ return {
209
+ "mode": self.mode,
210
+ "allowed": self.allowed,
211
+ "enforced": self.enforced,
212
+ "would_refuse": self.would_refuse,
213
+ "reason": self.reason,
214
+ "capability_ok": self.capability_ok,
215
+ "boundary_ok": self.boundary_ok,
216
+ "boundary_reason": self.boundary_reason,
217
+ "missing_capabilities": list(self.missing_capabilities),
218
+ "shadow": self.shadow,
219
+ "trust_ok": self.trust_ok,
220
+ "trust_reason": self.trust_reason,
221
+ }
222
+
223
+ def __getitem__(self, key: str) -> Any:
224
+ return self.as_dict()[key]
225
+
226
+ def get(self, key: str, default: Any = None) -> Any:
227
+ return self.as_dict().get(key, default)
228
+
229
+
230
+ class WorkerGovernor:
231
+ """Server-authoritative claim gate + completion audit for queue workers."""
232
+
233
+ def __init__(
234
+ self,
235
+ *,
236
+ directive_manager: Any = None,
237
+ feedback_store: Any = None,
238
+ self_model: Any = None,
239
+ delivery_router: Any = None, # awaitable(payload)->bool, guarded reach-out
240
+ proposal_store: Any = None,
241
+ skill_store: Any = None,
242
+ llm_router: Any = None,
243
+ boundary_required: bool = False,
244
+ ) -> None:
245
+ self._directives = directive_manager
246
+ self._feedback = feedback_store
247
+ self._self_model = self_model
248
+ self._deliver = delivery_router
249
+ self._proposal_store = proposal_store
250
+ self._skills = skill_store
251
+ self._llm = llm_router
252
+ self._boundary_required = bool(boundary_required)
253
+
254
+ def ready_for_live_claims(self) -> bool:
255
+ """Whether this instance is valid live claim authority."""
256
+
257
+ trust = getattr(self._self_model, "trust", None)
258
+ return bool(
259
+ self._boundary_required
260
+ and self._directives is not None
261
+ and self._self_model is not None
262
+ and trust is not None
263
+ )
264
+
265
+ # -- trust domain per job type ---------------------------------------
266
+ @staticmethod
267
+ def trust_domain(job_type: str) -> str:
268
+ return f"worker:{(job_type or 'custom').strip().lower()}"
269
+
270
+ # -- job classification (never trust the worker's own claim) ----------
271
+ @staticmethod
272
+ def _job_authorized_to_mutate(job: Any) -> bool:
273
+ """Was THIS job authorized to change state? Read-only jobs were not.
274
+
275
+ A job carries its risk tier and approval provenance in payload/tags
276
+ (set at post time by the approval policy). read_only risk => not
277
+ authorized; an approved or mutating-risk job => authorized.
278
+ """
279
+ payload = getattr(job, "payload", {}) or {}
280
+ tags = getattr(job, "tags", {}) or {}
281
+ # Risk/action fields are untrusted classification input. They may
282
+ # increase gating but can never grant mutation authority. Permission
283
+ # must be provenance stamped by the server-owned approval/policy
284
+ # plane and bound to the queued action.
285
+ return bool(
286
+ job_declares_effect(job)
287
+ and (
288
+ tags.get("approved_by")
289
+ or tags.get("auto_approved_by_policy")
290
+ )
291
+ )
292
+
293
+ def _job_action(self, job: Any):
294
+ """Build the DirectiveGuard Action for this job's subject.
295
+
296
+ Worker jobs are active autonomous work -> ACT capability (an
297
+ ACT-level "leave X alone" boundary must stop a worker touching X;
298
+ awareness/reads survive). high_risk on any mutating/outbound job so
299
+ the guard fails closed on ambiguity.
300
+ """
301
+ from apsimo.directives import Action
302
+ payload = getattr(job, "payload", {}) or {}
303
+ subject_keys = (
304
+ "description", "action_hint", "domain", "action", "command",
305
+ "script", "query", "endpoint", "url", "target_path", "path",
306
+ "target", "entity_id", "purpose",
307
+ )
308
+ # Only fields that name the proposed work belong in the boundary
309
+ # subject. Thought jobs also carry prompts, injected directives, and
310
+ # other control context. Passing the whole payload through ``args``
311
+ # made DirectiveGuard index that context as if the worker intended to
312
+ # act on it, so an injected boundary could match its own text and
313
+ # permanently hold an otherwise unrelated job.
314
+ subject_args = {
315
+ key: payload[key]
316
+ for key in subject_keys
317
+ if type(payload.get(key)) in (str, int, float)
318
+ and payload.get(key) not in (None, "")
319
+ }
320
+ text = " ".join(
321
+ str(subject_args[key]) for key in subject_keys
322
+ if key in subject_args
323
+ ).strip()
324
+ target = str(
325
+ subject_args.get("target_path") or subject_args.get("path")
326
+ or subject_args.get("endpoint") or subject_args.get("url")
327
+ or subject_args.get("target") or subject_args.get("entity_id")
328
+ or subject_args.get("domain") or ""
329
+ )
330
+ return Action(
331
+ kind="execute_tool",
332
+ text=text or str(getattr(job, "job_type", "")),
333
+ target=target,
334
+ entity_id=str(subject_args.get("entity_id") or ""),
335
+ tool_name=str(
336
+ subject_args.get("action") or subject_args.get("command")
337
+ or getattr(getattr(job, "job_type", None), "value", "")
338
+ or getattr(job, "job_type", "")
339
+ ),
340
+ args=subject_args,
341
+ high_risk=self._job_authorized_to_mutate(job),
342
+ )
343
+
344
+ @staticmethod
345
+ def _required_caps(job: Any) -> List[str]:
346
+ req: List[str] = []
347
+ try:
348
+ req.extend(job.required_capabilities())
349
+ except Exception:
350
+ pass
351
+ tags = getattr(job, "tags", {}) or {}
352
+ rc = tags.get("required_capability")
353
+ if rc:
354
+ req.append(str(rc))
355
+ return [c for c in req if c]
356
+
357
+ def _read_only_live_trial_status(
358
+ self,
359
+ domain: str,
360
+ ) -> tuple[bool, str]:
361
+ """Authorize one bounded, evidence-backed read-only live trial.
362
+
363
+ ``ask_first`` normally requires an owner decision. That creates a
364
+ bootstrap deadlock for no-effect worker domains: calibration can
365
+ prove they are safe enough to leave shadow, but they can never
366
+ produce the real, independently attested outcomes required to earn
367
+ ``act_first``. This bridge is deliberately narrower than approval:
368
+
369
+ * it is considered only for jobs already classified as no-effect;
370
+ * the domain needs clean, server-attested no-effect shadow evidence;
371
+ * any real failure, timeout, violation, or evidence gap closes it;
372
+ * every real attempt consumes one of a small, bounded trial budget.
373
+
374
+ Capability, boundary, route, owner, and global kill-switch checks are
375
+ separate claim gates and remain mandatory.
376
+ """
377
+
378
+ store = getattr(self._self_model, "store", None)
379
+ if store is None or not hasattr(store, "events"):
380
+ return False, "read_only_live_trial_store_unavailable"
381
+ try:
382
+ if (
383
+ hasattr(store, "active_evidence_gaps")
384
+ and store.active_evidence_gaps(domain)
385
+ ):
386
+ return False, "read_only_live_trial_evidence_gap"
387
+
388
+ try:
389
+ ask_min = int(os.environ.get("COLONY_TRUST_ASK_MIN_N", "3"))
390
+ except (TypeError, ValueError):
391
+ ask_min = 3
392
+ ask_min = max(1, min(ask_min, 100))
393
+ try:
394
+ trial_max = int(os.environ.get(
395
+ "COLONY_TRUST_READ_ONLY_TRIAL_MAX",
396
+ os.environ.get("COLONY_TRUST_ACT_MIN_N", "5"),
397
+ ))
398
+ except (TypeError, ValueError):
399
+ trial_max = 5
400
+ trial_max = max(1, min(trial_max, 20))
401
+
402
+ history = store.events(
403
+ domain,
404
+ include_shadow=True,
405
+ include_invalid=True,
406
+ include_unavailable=True,
407
+ )
408
+ shadow_history = [
409
+ event for event in history if bool(event.get("shadow"))
410
+ ]
411
+ clean_calibration = []
412
+ for event in shadow_history:
413
+ evidence = event.get("evidence")
414
+ if not isinstance(evidence, dict):
415
+ continue
416
+ if (
417
+ event.get("source") == "task_queue.governor"
418
+ and event.get("outcome") == "success"
419
+ and not bool(event.get("violation"))
420
+ and event.get("evidence_status") == "verified"
421
+ and event.get("outcome_contract")
422
+ == "colony.worker-outcome/v1"
423
+ and evidence.get("schema") == "colony.worker-outcome/v1"
424
+ and evidence.get("success_attested") is True
425
+ and evidence.get("effectful") is False
426
+ and event.get("valid") is not False
427
+ and event.get("evidence_available") is not False
428
+ ):
429
+ clean_calibration.append(event)
430
+ if len(clean_calibration) < ask_min:
431
+ return (
432
+ False,
433
+ "read_only_live_trial_calibration_insufficient_"
434
+ f"{len(clean_calibration)}_of_{ask_min}",
435
+ )
436
+
437
+ real_history = [
438
+ event for event in history if not bool(event.get("shadow"))
439
+ ]
440
+ if any(
441
+ bool(event.get("violation"))
442
+ or event.get("outcome") in {"failure", "timeout"}
443
+ or event.get("valid") is False
444
+ or event.get("evidence_available") is False
445
+ for event in real_history
446
+ ):
447
+ return False, "read_only_live_trial_negative_evidence"
448
+ trial_count = len(real_history)
449
+ if trial_count >= trial_max:
450
+ return (
451
+ False,
452
+ f"read_only_live_trial_budget_exhausted_{trial_max}",
453
+ )
454
+ return (
455
+ True,
456
+ f"bounded_read_only_live_trial_{trial_count + 1}_of_"
457
+ f"{trial_max}",
458
+ )
459
+ except Exception:
460
+ logger.warning(
461
+ "worker read-only live-trial evidence check failed closed",
462
+ exc_info=True,
463
+ )
464
+ return False, "read_only_live_trial_check_failed_closed"
465
+
466
+ # -- claim gate -------------------------------------------------------
467
+ def evaluate_claim(self, job: Any, worker_capabilities: Iterable[str],
468
+ worker_node_id: str = "") -> ClaimVerdict:
469
+ """Re-decide, server-side, whether this worker may run this job.
470
+
471
+ Returns an immutable :class:`ClaimVerdict`. In shadow, ``allowed`` is
472
+ always true (calibration), while ``would_refuse`` records what live
473
+ mode would do.
474
+ """
475
+ mode = workers_mode()
476
+ if mode == "off":
477
+ # Nothing is checked in mode "off": say so (None = unchecked)
478
+ # rather than fabricating capability/boundary/trust passes.
479
+ return ClaimVerdict(
480
+ mode="off", allowed=True, enforced=False,
481
+ would_refuse=False, reason="governor_off",
482
+ capability_ok=None, boundary_ok=None,
483
+ boundary_reason="governor_off_unchecked", shadow=False,
484
+ trust_ok=None, trust_reason="governor_off_unchecked",
485
+ )
486
+
487
+ caps = set(worker_capabilities or [])
488
+ required = self._required_caps(job)
489
+ missing = [c for c in required if c not in caps]
490
+ capability_ok = not missing
491
+
492
+ boundary_ok = not self._boundary_required
493
+ boundary_reason = (
494
+ "boundary_checker_not_required"
495
+ if boundary_ok else "boundary_checker_unavailable"
496
+ )
497
+ job_action = self._job_action(job)
498
+ if self._directives is not None:
499
+ try:
500
+ verdict = self._directives.check(job_action)
501
+ if type(getattr(verdict, "allowed", None)) is not bool:
502
+ raise TypeError("directive verdict allowed must be boolean")
503
+ boundary_ok = verdict.allowed
504
+ boundary_reason = str(getattr(verdict, "reason", "") or "ok")
505
+ except Exception:
506
+ # A configured boundary dependency is part of the live claim
507
+ # authority. Its failure cannot be interpreted as permission.
508
+ # Shadow remains observational, but records the exact claim it
509
+ # would refuse after graduation.
510
+ boundary_ok = False
511
+ boundary_reason = "boundary_check_failed_closed"
512
+ logger.warning(
513
+ "worker claim boundary check failed closed",
514
+ exc_info=True,
515
+ )
516
+
517
+ jt = self._job_type_value(job)
518
+ domain = self.trust_domain(jt)
519
+ tags = getattr(job, "tags", {}) or {}
520
+ approved = bool(
521
+ tags.get("approved_by") or tags.get("auto_approved_by_policy")
522
+ )
523
+ effectful = job_declares_effect(job)
524
+ trust = getattr(self._self_model, "trust", None)
525
+ if trust is None:
526
+ trust_ok = False
527
+ trust_reason = "worker_trust_unavailable"
528
+ else:
529
+ try:
530
+ from apsimo.self_model.trust import floor_class
531
+
532
+ stage = str(trust.stage(domain) or "shadow")
533
+ confidence = float(trust.confidence(domain))
534
+ try:
535
+ threshold = float(os.environ.get(
536
+ "COLONY_TRUST_ACT_THRESHOLD", "0.8"
537
+ ))
538
+ except ValueError:
539
+ threshold = 0.8
540
+ floor = floor_class(
541
+ f"{job_action.text} {job_action.target} "
542
+ f"{job_action.tool_name}"
543
+ )
544
+ if mode == "shadow" and effectful:
545
+ trust_ok = False
546
+ trust_reason = "shadow_effect_execution_disabled"
547
+ elif effectful and not approved:
548
+ trust_ok = False
549
+ trust_reason = "action_approval_required"
550
+ elif floor and not approved:
551
+ trust_ok = False
552
+ trust_reason = f"immutable_floor_{floor}"
553
+ elif (
554
+ mode == "live"
555
+ and stage == "ask_first"
556
+ and not effectful
557
+ and not approved
558
+ ):
559
+ trust_ok, trust_reason = (
560
+ self._read_only_live_trial_status(domain)
561
+ )
562
+ else:
563
+ trust_ok = approved or (
564
+ stage == "act_first" and confidence >= threshold
565
+ )
566
+ trust_reason = (
567
+ f"worker_trust_{stage}_confidence_{confidence:.3f}"
568
+ + ("_approved" if approved else "")
569
+ )
570
+ except Exception:
571
+ trust_ok = False
572
+ trust_reason = "worker_trust_check_failed_closed"
573
+
574
+ would_refuse = not (capability_ok and boundary_ok and trust_ok)
575
+ if would_refuse and not capability_ok:
576
+ reason = f"worker lacks required capabilities: {missing}"
577
+ elif would_refuse and boundary_reason in {
578
+ "boundary_checker_unavailable", "boundary_check_failed_closed",
579
+ }:
580
+ reason = f"job boundary unavailable: {boundary_reason}"
581
+ elif would_refuse:
582
+ if not boundary_ok:
583
+ reason = f"job subject under boundary: {boundary_reason}"
584
+ else:
585
+ reason = f"worker autonomy not earned: {trust_reason}"
586
+ else:
587
+ reason = "ok"
588
+
589
+ shadow = mode == "shadow"
590
+ allowed = (
591
+ not would_refuse
592
+ if mode == "live"
593
+ else (not effectful if mode == "shadow" else True)
594
+ )
595
+
596
+ # A refused (live) or would-be-refused (shadow) claim is journaled;
597
+ # a clean claim is not (it becomes an outcome at completion).
598
+ if would_refuse:
599
+ self._journal(
600
+ self.trust_domain(getattr(job, "job_type", None)
601
+ and job.job_type.value if hasattr(
602
+ getattr(job, "job_type", None), "value")
603
+ else getattr(job, "job_type", "custom")),
604
+ f"worker {worker_node_id} claim of {self._job_id(job)}",
605
+ reasoning=reason,
606
+ decision="blocked" if not allowed else "noted",
607
+ ref=self._job_id(job))
608
+ return ClaimVerdict(
609
+ mode=mode,
610
+ allowed=allowed,
611
+ enforced=(mode == "live" or (mode == "shadow" and not allowed)),
612
+ would_refuse=would_refuse,
613
+ reason=reason,
614
+ capability_ok=capability_ok,
615
+ boundary_ok=boundary_ok,
616
+ boundary_reason=boundary_reason,
617
+ missing_capabilities=tuple(missing),
618
+ shadow=shadow,
619
+ trust_ok=trust_ok,
620
+ trust_reason=trust_reason,
621
+ )
622
+
623
+ # -- completion audit (never trust the report) ------------------------
624
+ def audit_report(self, job: Any, report: Dict[str, Any]) -> Dict[str, Any]:
625
+ """Cross-check the worker's structured report against its authority.
626
+
627
+ Verdict: 'clean' | 'violation' | 'unverified'. A violation from any
628
+ check wins (fail loud).
629
+ """
630
+ report = report or {}
631
+ findings: List[str] = []
632
+ ok = True
633
+
634
+ authorized = self._job_authorized_to_mutate(job)
635
+ ops = [str(o).strip().lower() for o in (report.get("operations") or [])]
636
+ reported_mutation = (
637
+ bool(set(ops) & _MUTATE_OPS)
638
+ or int(report.get("commits") or 0) > 0
639
+ or bool(report.get("files_written"))
640
+ or bool(report.get("deletions"))
641
+ )
642
+ if reported_mutation and not authorized:
643
+ ok = False
644
+ findings.append(
645
+ "worker reported a mutation on a job not authorized to change "
646
+ f"state (ops={sorted(set(ops) & _MUTATE_OPS) or 'commits/files'})")
647
+ if report.get("force_push"):
648
+ ok = False
649
+ findings.append("force push reported (never allowed)")
650
+
651
+ # Capability escalation: a worker reporting it used a capability the
652
+ # job never required and the worker never advertised is suspicious.
653
+ # (Kept advisory unless it co-occurs with a mutation.)
654
+ if reported_mutation and not authorized and report.get("escalated"):
655
+ findings.append("worker reported escalating its own scope")
656
+
657
+ has_report = bool(report) and any(
658
+ k in report for k in (
659
+ "summary", "operations", "result", "output",
660
+ "execution_result",
661
+ )
662
+ )
663
+ if not ok:
664
+ verdict = "violation"
665
+ elif has_report:
666
+ verdict = "clean"
667
+ else:
668
+ verdict = "unverified"
669
+
670
+ result = {"verdict": verdict, "findings": findings,
671
+ "authorized_to_mutate": authorized,
672
+ "reported_mutation": reported_mutation}
673
+ if verdict == "violation":
674
+ logger.warning("WORKER SCOPE VIOLATION on %s: %s",
675
+ self._job_id(job), "; ".join(findings)[:400])
676
+ return result
677
+
678
+ # -- outcome recording (feedback + self-model + journal + skills) -----
679
+ @staticmethod
680
+ def classify_completion_outcome(
681
+ report: Dict[str, Any],
682
+ verdict: str,
683
+ ) -> tuple[str, str]:
684
+ """Classify semantic work outcome independently of HTTP completion.
685
+
686
+ ``POST .../complete`` only says the worker stopped and returned a
687
+ payload. It is not evidence that useful work succeeded. Success needs
688
+ a clean audit plus an explicit verified completion marker. Policy
689
+ skips, cancellations, unknown/malformed terminal states, and ordinary
690
+ unverified reports are neutral: journalable, but never competence or
691
+ earned-autonomy evidence.
692
+ """
693
+
694
+ payload = report if isinstance(report, dict) else {}
695
+ execution_result = (
696
+ payload.get("execution_result")
697
+ if isinstance(payload.get("execution_result"), dict)
698
+ else {}
699
+ )
700
+ semantic = str(
701
+ payload.get("status") or payload.get("outcome")
702
+ or execution_result.get("terminal_outcome") or ""
703
+ ).strip().lower()
704
+ if semantic == "succeeded":
705
+ semantic = "completed"
706
+ action_plane = (
707
+ payload.get("action_plane")
708
+ if isinstance(payload.get("action_plane"), dict)
709
+ else {}
710
+ )
711
+ action_state = str(action_plane.get("state") or "").strip().lower()
712
+ verification = str(
713
+ payload.get("verification_status") or ""
714
+ ).strip().lower()
715
+
716
+ if verdict == "violation":
717
+ return "failure", "scope_violation"
718
+ failure_markers = {"failed", "failure", "error"}
719
+ if semantic in failure_markers or action_state in failure_markers:
720
+ return "failure", "reported_failure"
721
+ neutral_markers = {
722
+ "skipped", "skip", "cancelled", "canceled", "unknown",
723
+ "pending", "proposed", "gated", "accepted",
724
+ }
725
+ if semantic in neutral_markers or action_state in neutral_markers:
726
+ return "neutral", f"terminal_{semantic or action_state}"
727
+ if verdict != "clean":
728
+ return "neutral", f"audit_{verdict or 'unverified'}"
729
+
730
+ verified_completion = (
731
+ (
732
+ execution_result.get("schema") == "ExecutionResultV1"
733
+ and str(
734
+ execution_result.get("terminal_outcome") or ""
735
+ ).lower() == "succeeded"
736
+ )
737
+ or semantic == "verified"
738
+ or (
739
+ semantic == "completed"
740
+ and (
741
+ action_state in {"verified", "completed"}
742
+ or verification == "verified"
743
+ )
744
+ )
745
+ )
746
+ if verified_completion:
747
+ return "success", "verified_completion"
748
+ if semantic == "completed":
749
+ return "neutral", "completed_without_verification"
750
+ return "neutral", "unknown_semantic_outcome"
751
+
752
+ async def record_outcome(self, job: Any, report: Dict[str, Any],
753
+ verdict: str, *, outcome: Optional[str] = None,
754
+ latency: Optional[float] = None,
755
+ attempts: int = 0,
756
+ event_id: Optional[str] = None,
757
+ event_mode: Optional[str] = None,
758
+ success_attested: bool = False) -> Dict[str, Any]:
759
+ """Fold an audited completion into the trust/accountability layer."""
760
+ report = report or {}
761
+ mode = (
762
+ str(event_mode).strip().lower()
763
+ if str(event_mode or "").strip().lower() in {"off", "shadow", "live"}
764
+ else workers_mode()
765
+ )
766
+ if mode == "off":
767
+ return {"recorded": False, "reason": "governor_off"}
768
+ jt = self._job_type_value(job)
769
+ domain = self.trust_domain(jt)
770
+ shadow = mode != "live"
771
+
772
+ if outcome is None:
773
+ outcome, outcome_reason = self.classify_completion_outcome(
774
+ report, verdict
775
+ )
776
+ else:
777
+ outcome = str(outcome).strip().lower()
778
+ if outcome not in {"success", "failure", "neutral"}:
779
+ outcome = "neutral"
780
+ outcome_reason = "explicit_outcome"
781
+ if outcome == "success":
782
+ derived, derived_reason = self.classify_completion_outcome(
783
+ report, verdict
784
+ )
785
+ if derived != "success":
786
+ outcome = derived
787
+ outcome_reason = f"rejected_explicit_success:{derived_reason}"
788
+
789
+ stated = report.get("confidence")
790
+ try:
791
+ stated = float(stated) if stated is not None else None
792
+ except (TypeError, ValueError):
793
+ stated = None
794
+
795
+ # Self-model / trust engine: the earned-autonomy evidence. Competence
796
+ # and its deterministic trust transition are separate durable stages:
797
+ # a replayed event re-runs trust reconciliation without duplicating
798
+ # the immutable competence row.
799
+ duplicate = False
800
+ if outcome != "neutral" and self._self_model is None and event_id:
801
+ raise RuntimeError("worker competence store is unavailable")
802
+ if outcome != "neutral" and self._self_model is not None:
803
+ try:
804
+ recorded = self._self_model.record(
805
+ domain, outcome, latency_secs=latency,
806
+ shadow=shadow, violation=(verdict == "violation"),
807
+ stated_confidence=stated,
808
+ source="task_queue.governor",
809
+ source_ref=self._job_id(job),
810
+ event_key=event_id,
811
+ # A worker report is observed evidence. It is never
812
+ # promoted to externally verified merely because the
813
+ # worker wrote status="verified" in its own payload.
814
+ evidence_status=(
815
+ "verified"
816
+ if outcome == "success" and success_attested
817
+ else "observed"
818
+ ),
819
+ outcome_contract="colony.worker-outcome/v1",
820
+ evidence={
821
+ "schema": "colony.worker-outcome/v1",
822
+ "audit_verdict": verdict,
823
+ "classification": outcome_reason,
824
+ "attempts": max(0, int(attempts or 0)),
825
+ "event_id": event_id,
826
+ "success_attested": bool(success_attested),
827
+ "effectful": bool(job_declares_effect(job)),
828
+ },
829
+ defer_trust=True,
830
+ )
831
+ if event_id and recorded is False:
832
+ store = getattr(self._self_model, "store", None)
833
+ if (
834
+ store is not None
835
+ and hasattr(store, "has_event_key")
836
+ and store.has_event_key(
837
+ "task_queue.governor", event_id,
838
+ )
839
+ ):
840
+ duplicate = True
841
+ else:
842
+ raise RuntimeError(
843
+ "worker competence event was not durably recorded"
844
+ )
845
+ if hasattr(self._self_model, "reconcile_trust"):
846
+ self._self_model.reconcile_trust(domain)
847
+ else:
848
+ trust = getattr(self._self_model, "trust", None)
849
+ if trust is not None:
850
+ trust.after_outcome(domain)
851
+ except Exception:
852
+ logger.warning(
853
+ "worker self-model record failed",
854
+ exc_info=True,
855
+ )
856
+ raise
857
+
858
+ # Worker self-reports are never owner feedback. TypeFeedbackStore is
859
+ # reserved for actual owner reactions and must not amplify a worker's
860
+ # own success claim into the trust confidence multiplier.
861
+
862
+ journal_id = self._journal(
863
+ domain,
864
+ f"job {self._job_id(job)} ({jt}) completed",
865
+ reasoning=(
866
+ f"audit verdict: {verdict}; semantic outcome: {outcome} "
867
+ f"({outcome_reason})"
868
+ ),
869
+ confidence=stated,
870
+ reversibility="recoverable" if self._job_authorized_to_mutate(job)
871
+ else "reversible",
872
+ decision="noted", outcome=outcome, ref=self._job_id(job),
873
+ event_key=event_id,
874
+ )
875
+ if event_id and journal_id < 0:
876
+ raise RuntimeError("worker action journal is unavailable")
877
+
878
+ # Owner-facing safety notice precedes optional LLM distillation and
879
+ # carries a stable proposal ID so crash-window retries are deduplicable
880
+ # downstream. Its own durable receipt lets a failed first delivery be
881
+ # retried while suppressing routine outbox replays after success.
882
+ if verdict == "violation" and self._deliver is not None:
883
+ receipt_key = (
884
+ f"{event_id}:violation-notice-delivered"
885
+ if event_id else None
886
+ )
887
+ journal = getattr(self._self_model, "journal", None)
888
+ already_delivered = bool(
889
+ receipt_key
890
+ and journal is not None
891
+ and hasattr(journal, "has_event_key")
892
+ and journal.has_event_key(receipt_key)
893
+ )
894
+ if not already_delivered:
895
+ await self._notify_violation(job, report, event_id=event_id)
896
+ if receipt_key:
897
+ receipt_id = self._journal(
898
+ domain,
899
+ f"violation notice delivered for worker event "
900
+ f"{event_id}",
901
+ reasoning="durable worker-notice delivery receipt",
902
+ decision="noted",
903
+ outcome="delivered",
904
+ ref=self._job_id(job),
905
+ event_key=receipt_key,
906
+ )
907
+ if receipt_id < 0:
908
+ raise RuntimeError(
909
+ "worker violation delivery receipt unavailable"
910
+ )
911
+
912
+ # The durable queue outbox may replay an event after a timeout or
913
+ # restart. Trust reconciliation and the violation receipt above are
914
+ # intentionally repeatable; LLM skill distillation is not.
915
+ if duplicate:
916
+ return {
917
+ "recorded": False,
918
+ "duplicate": True,
919
+ "event_id": event_id,
920
+ }
921
+
922
+ # Skill distillation on genuine successes (retry-success / novel).
923
+ if outcome == "success" and verdict != "violation":
924
+ await self._maybe_distill(job, report, attempts)
925
+ return {
926
+ "recorded": not duplicate,
927
+ "duplicate": duplicate,
928
+ "event_id": event_id,
929
+ }
930
+
931
+ async def _maybe_distill(self, job: Any, report: Dict[str, Any],
932
+ attempts: int) -> None:
933
+ if self._skills is None or self._llm is None:
934
+ return
935
+ try:
936
+ from apsimo.skills_memory import (
937
+ should_distill, distill_from_completion,
938
+ )
939
+ result_text = str(report.get("summary")
940
+ or report.get("result") or "")
941
+ if not should_distill(attempts, result_text, self._skills):
942
+ return
943
+ payload = getattr(job, "payload", {}) or {}
944
+ task_text = str(payload.get("description")
945
+ or payload.get("action_hint") or "")
946
+ await distill_from_completion(
947
+ self._llm, self._skills,
948
+ domain=self.trust_domain(self._job_type_value(job)),
949
+ task_text=task_text, result_text=result_text,
950
+ source_ref=self._job_id(job))
951
+ except Exception:
952
+ logger.debug("worker skill distill failed", exc_info=True)
953
+
954
+ async def _notify_violation(
955
+ self,
956
+ job: Any,
957
+ report: Dict[str, Any],
958
+ *,
959
+ event_id: Optional[str] = None,
960
+ ) -> None:
961
+ try:
962
+ from apsimo.proposals import Proposal, proposal_to_payload
963
+ findings = "; ".join(
964
+ self.audit_report(job, report).get("findings", [])[:5])
965
+ notice_key = str(
966
+ event_id or f"worker-violation:{self._job_id(job)}"
967
+ )
968
+ prop = Proposal(
969
+ id=("prop-worker-" + hashlib.sha256(
970
+ notice_key.encode()
971
+ ).hexdigest()[:16]),
972
+ title=f"Worker scope violation on job {self._job_id(job)}",
973
+ finding=(f"A queue worker exceeded its authorized scope: "
974
+ f"{findings}. I flagged and recorded it; the job "
975
+ "type has been demoted from autonomous execution."),
976
+ why_it_helps="protects you from an over-reaching worker",
977
+ suggested_action="Review the flagged job and worker",
978
+ source=self._job_id(job), initiative_type="proposal",
979
+ confidence=0.95)
980
+ if self._proposal_store is not None:
981
+ insert_once = getattr(
982
+ self._proposal_store, "add_if_absent", None,
983
+ )
984
+ if callable(insert_once):
985
+ insert_once(prop)
986
+ else:
987
+ self._proposal_store.add(prop)
988
+ delivered = await self._deliver(proposal_to_payload(prop))
989
+ if delivered is not True:
990
+ raise RuntimeError("worker violation notice was not delivered")
991
+ except Exception:
992
+ logger.debug("worker violation notify failed", exc_info=True)
993
+ raise
994
+
995
+ # -- observability ----------------------------------------------------
996
+ def status(self) -> Dict[str, Any]:
997
+ out: Dict[str, Any] = {
998
+ "mode": workers_mode(),
999
+ "boundary_required": self._boundary_required,
1000
+ "directive_dependency_available": self._directives is not None,
1001
+ "ready_for_live_claims": self.ready_for_live_claims(),
1002
+ }
1003
+ trust = getattr(self._self_model, "trust", None)
1004
+ if trust is not None:
1005
+ try:
1006
+ out["worker_domains"] = [
1007
+ s for s in trust.snapshot()
1008
+ if str(s.get("domain", "")).startswith("worker:")]
1009
+ except Exception:
1010
+ pass
1011
+ return out
1012
+
1013
+ # -- helpers ----------------------------------------------------------
1014
+ def _journal(self, domain: str, description: str, **kw: Any) -> int:
1015
+ journal = getattr(self._self_model, "journal", None)
1016
+ if journal is None:
1017
+ return -1
1018
+ try:
1019
+ return int(journal.record(domain, description, **kw))
1020
+ except Exception:
1021
+ logger.debug("worker journal failed", exc_info=True)
1022
+ return -1
1023
+
1024
+ @staticmethod
1025
+ def _job_id(job: Any) -> str:
1026
+ return str(getattr(job, "job_id", "") or getattr(job, "id", "") or "?")
1027
+
1028
+ @staticmethod
1029
+ def _job_type_value(job: Any) -> str:
1030
+ jt = getattr(job, "job_type", "custom")
1031
+ return jt.value if hasattr(jt, "value") else str(jt)