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/turns/audio.py ADDED
@@ -0,0 +1,134 @@
1
+ """Bounded supplied audio and fallible transcript segments, never remote fetches."""
2
+ from __future__ import annotations
3
+
4
+ import base64
5
+ from datetime import datetime
6
+ import hashlib
7
+ import io
8
+ import math
9
+ import wave
10
+
11
+ MAX_AUDIO_BYTES = 4 * 1024 * 1024
12
+ MAX_AUDIO_SECONDS = 60
13
+
14
+
15
+ def decode_audio(block):
16
+ item = block.get('input_audio')
17
+ if (not isinstance(item, dict) or set(item) != {'data', 'format'}
18
+ or item['format'] != 'wav' or not isinstance(item['data'], str)
19
+ or len(item['data']) > MAX_AUDIO_BYTES * 4 // 3 + 16):
20
+ raise ValueError('unsupported_or_oversized_audio')
21
+ data = base64.b64decode(item['data'], validate=True)
22
+ if len(data) > MAX_AUDIO_BYTES:
23
+ raise ValueError('oversized_audio')
24
+ with wave.open(io.BytesIO(data), 'rb') as clip:
25
+ rate, frames = clip.getframerate(), clip.getnframes()
26
+ if (clip.getnchannels() != 1 or clip.getsampwidth() != 2 or clip.getcomptype() != 'NONE'
27
+ or not 8000 <= rate <= 48000 or not 0 < frames <= rate * MAX_AUDIO_SECONDS
28
+ or len(clip.readframes(frames + 1)) != frames * 2):
29
+ raise ValueError('unsupported_audio_encoding_or_duration')
30
+ return data, {'sample_rate': rate, 'channels': 1, 'sample_width_bytes': 2,
31
+ 'duration_ms': frames * 1000 / rate, 'frame_count': frames}
32
+
33
+
34
+ def transcript(block, data, duration_ms):
35
+ """Validate supplied recognition lineage; clocks are not inferred from text."""
36
+ if (not isinstance(block, dict) or set(block) != {'type', 'audio_sha256', 'segments',
37
+ 'recognizer', 'received_at', 'captured_at'} or block['type'] != 'audio_transcript'
38
+ or block['audio_sha256'] != hashlib.sha256(data).hexdigest()):
39
+ raise ValueError('audio_transcript_parent_mismatch')
40
+ segments = block['segments']
41
+ if not isinstance(segments, list) or not 1 <= len(segments) <= 32:
42
+ raise ValueError('unsupported_transcript_segments')
43
+ previous = 0
44
+ for segment in segments:
45
+ if (not isinstance(segment, dict) or set(segment) != {'start_ms', 'end_ms', 'text'}
46
+ or not isinstance(segment['text'], str) or not 1 <= len(segment['text']) <= 12000
47
+ or any(type(segment[key]) not in (int, float) or not math.isfinite(segment[key])
48
+ for key in ('start_ms', 'end_ms'))
49
+ or not previous <= segment['start_ms'] < segment['end_ms'] <= duration_ms + .001):
50
+ raise ValueError('invalid_transcript_segment')
51
+ previous = segment['end_ms']
52
+ for key in ('received_at', 'captured_at'):
53
+ value = block[key]
54
+ if value is None and key == 'captured_at':
55
+ continue
56
+ if not isinstance(value, str) or len(value) > 64 or datetime.fromisoformat(value.replace('Z', '+00:00')).tzinfo is None:
57
+ raise ValueError('invalid_audio_clock')
58
+ model = block['recognizer']
59
+ if (not isinstance(model, dict) or set(model) != {'model_id', 'model_revision'}
60
+ or any(not isinstance(value, str) or not 1 <= len(value) <= 256 for value in model.values())):
61
+ raise ValueError('invalid_recognizer_provenance')
62
+ return {**block, 'asset_id': 'sha256:' + block['audio_sha256'],
63
+ 'epistemic_state': 'derived_unverified', 'timing_basis': 'supplied_clip_offsets',
64
+ 'confidence': None}
65
+
66
+
67
+ def _render(content):
68
+ """One rendering for source indexing, exact claim spans and recall."""
69
+ if isinstance(content, str):
70
+ return content, []
71
+ parts, segments, length = [], [], 0
72
+
73
+ def append(text):
74
+ nonlocal length
75
+ start = length + bool(parts)
76
+ parts.append(text)
77
+ length = start + len(text)
78
+ return start
79
+
80
+ blocks = content if isinstance(content, list) else []
81
+ for index, block in enumerate(blocks):
82
+ if not isinstance(block, dict):
83
+ continue
84
+ if block.get('type') in {'text', 'input_text', 'output_text'} and isinstance(block.get('text'), str):
85
+ append(block['text'])
86
+ elif block.get('type') == 'audio_transcript' and block.get('epistemic_state') == 'derived_unverified':
87
+ parent = blocks[index - 1] if index else {}
88
+ owned = (isinstance(parent, dict) and parent.get('type') == 'audio'
89
+ and parent.get('asset_id') == block['asset_id'])
90
+ for segment_index, segment in enumerate(block.get('segments', [])):
91
+ prefix = (f"Unverified machine transcript [{segment['start_ms'] / 1000:.3f}.."
92
+ f"{segment['end_ms'] / 1000:.3f}s of {block['asset_id']}]: ")
93
+ start = append(prefix + segment['text']) + len(prefix)
94
+ if owned:
95
+ segments.append({'block_index': index, 'segment_index': segment_index,
96
+ 'asset_id': block['asset_id'], 'start_ms': segment['start_ms'], 'end_ms': segment['end_ms'],
97
+ 'source_start': start, 'source_end': start + len(segment['text']),
98
+ **{key: block[key] for key in ('recognizer', 'received_at', 'captured_at',
99
+ 'timing_basis', 'confidence')}})
100
+ return '\n'.join(parts), segments
101
+
102
+
103
+ def source_text(content):
104
+ """Text-only recall of retained evidence; raw audio never enters prompts."""
105
+ return _render(content)[0]
106
+
107
+
108
+ def claim_message(message):
109
+ """An internal extraction view, never a second canonical user message.
110
+
111
+ Complete surrounding text remains available to interpret narrative scope.
112
+ Only retained, paired ASR word ranges become eligible derived evidence.
113
+ """
114
+ if isinstance(message.get('content'), str):
115
+ return message
116
+ text, segments = _render(message.get('content'))
117
+ return {**message, 'content': text, '_audio_segments': segments} if segments else None
118
+
119
+
120
+ def claim_basis(message, start, end):
121
+ for segment in message.get('_audio_segments', []):
122
+ if segment['source_start'] <= start < end <= segment['source_end']:
123
+ return {'epistemic_state': 'derived_unverified', 'source_modality': 'audio_transcript',
124
+ 'segment': {**segment, 'evidence_start': start - segment['source_start'],
125
+ 'evidence_end': end - segment['source_start']}}
126
+ return None
127
+
128
+
129
+ def evidence_metadata(message):
130
+ content = message.get('content')
131
+ if isinstance(content, list) and any(isinstance(block, dict) and block.get('type') == 'audio_transcript'
132
+ and block.get('epistemic_state') == 'derived_unverified' for block in content):
133
+ return {'epistemic_state': 'derived_unverified', 'source_modality': 'audio_transcript'}
134
+ return {}
@@ -0,0 +1,235 @@
1
+ """Bounded PDF extraction from owned bytes, never paths supplied by a caller.
2
+
3
+ This file also runs as a standalone isolated child. Keep imports at module load
4
+ in the standard library so resource limits precede loading the PDF parser.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import asyncio
9
+ import base64
10
+ import io
11
+ import json
12
+ import logging
13
+ from pathlib import Path
14
+ import sys
15
+
16
+ MAX_DOCUMENT_BYTES = 4 * 1024 * 1024
17
+ MAX_PAGES = 64
18
+ MAX_PAGE_STREAM_BYTES = 2 * 1024 * 1024
19
+ MAX_PAGE_CHARS = 32000
20
+ MAX_TEXT_CHARS = 200000
21
+ MAX_RESULT_BYTES = 6 * MAX_TEXT_CHARS + 65536
22
+ MAX_PARSE_SECONDS = 15
23
+ MAX_MEMORY_BYTES = 384 * 1024 * 1024
24
+ MEMORY_SAMPLE_INTERVAL_MS = 25
25
+ VERSION = 'source-pdf-text-v1'
26
+
27
+
28
+ def decode_document(block):
29
+ """Only explicit inline PDF bytes are supported by this ingestion contract."""
30
+ item = block.get('input_document')
31
+ if (set(block) != {'type', 'input_document'} or not isinstance(item, dict)
32
+ or set(item) != {'mime_type', 'data'} or item.get('mime_type') != 'application/pdf'
33
+ or not isinstance(item.get('data'), str)):
34
+ raise ValueError('unsupported_document_input')
35
+ if len(item['data']) > ((MAX_DOCUMENT_BYTES + 2) // 3) * 4:
36
+ raise ValueError('document_bytes_exceed_limit')
37
+ try:
38
+ data = base64.b64decode(item['data'], validate=True)
39
+ except (ValueError, TypeError):
40
+ raise ValueError('invalid_document_encoding') from None
41
+ if len(data) > MAX_DOCUMENT_BYTES:
42
+ raise ValueError('document_bytes_exceed_limit')
43
+ # This is a bounded admission signature check, not a PDF parser. Malformed
44
+ # PDF-looking originals remain retained with an honest extraction failure.
45
+ if not data.startswith(b'%PDF-'):
46
+ raise ValueError('unsupported_document_format')
47
+ return data
48
+
49
+
50
+ def disposition(status, reason=None, *, parser_version=None, page_count=None, pages=None):
51
+ return {'version': VERSION, 'parser': 'pypdf', 'parser_version': parser_version,
52
+ 'status': status, 'reason': reason, 'page_count': page_count,
53
+ 'pages': pages or [], 'ocr_performed': False,
54
+ 'epistemic_state': 'derived_unverified'}
55
+
56
+
57
+ def _extract(data):
58
+ """Called only inside the resource-limited child, including in tests."""
59
+ try:
60
+ import pypdf
61
+ except ImportError:
62
+ return disposition('failed', 'parser_unavailable')
63
+ version = pypdf.__version__
64
+ if not data.startswith(b'%PDF-') or len(data) > MAX_DOCUMENT_BYTES:
65
+ return disposition('failed', 'invalid_retained_document', parser_version=version)
66
+ try:
67
+ reader = pypdf.PdfReader(io.BytesIO(data), strict=True)
68
+ if reader.is_encrypted:
69
+ return disposition('unsupported', 'encrypted_pdf', parser_version=version)
70
+ count = len(reader.pages)
71
+ if not 0 < count <= MAX_PAGES:
72
+ return disposition('unsupported', 'page_count_exceeds_limit' if count else 'empty_pdf',
73
+ parser_version=version, page_count=count)
74
+ pages, total = [], 0
75
+ for number, page in enumerate(reader.pages, 1):
76
+ contents = page.get_contents()
77
+ # Decompression itself is inside the memory/CPU fence.
78
+ if contents is not None and len(contents.get_data()) > MAX_PAGE_STREAM_BYTES:
79
+ return disposition('unsupported', 'page_stream_exceeds_limit', parser_version=version,
80
+ page_count=count)
81
+ text = page.extract_text() or ''
82
+ total += len(text)
83
+ if len(text) > MAX_PAGE_CHARS or total > MAX_TEXT_CHARS:
84
+ return disposition('unsupported', 'extracted_text_exceeds_limit', parser_version=version,
85
+ page_count=count)
86
+ pages.append({'page': number, 'text': text,
87
+ 'status': 'text' if text.strip() else 'no_extractable_text'})
88
+ text_pages = sum(page['status'] == 'text' for page in pages)
89
+ return disposition('complete' if text_pages == count else 'partial' if text_pages else 'unsupported',
90
+ None if text_pages == count else 'no_extractable_text_on_some_pages' if text_pages
91
+ else 'no_extractable_text', parser_version=version, page_count=count, pages=pages)
92
+ except MemoryError:
93
+ return disposition('unsupported', 'parser_memory_limit', parser_version=version)
94
+ except Exception:
95
+ # Parser errors can contain document text. Persist a code, not that text.
96
+ return disposition('failed', 'invalid_or_unreadable_pdf', parser_version=version)
97
+
98
+
99
+ def _child():
100
+ logging.disable(logging.CRITICAL)
101
+ memory_control = 'address_space'
102
+ try:
103
+ import resource
104
+ resource.setrlimit(resource.RLIMIT_CPU, (10, 10))
105
+ resource.setrlimit(resource.RLIMIT_CORE, (0, 0))
106
+ try:
107
+ resource.setrlimit(resource.RLIMIT_AS, (MAX_MEMORY_BYTES, MAX_MEMORY_BYTES))
108
+ except (ValueError, OSError):
109
+ if sys.platform != 'darwin' or sys.argv[1:] != ['--rss-guarded']:
110
+ raise
111
+ memory_control = 'sampled_rss'
112
+ except (ImportError, ValueError, OSError):
113
+ result = disposition('unsupported', 'parser_resource_limits_unavailable')
114
+ else:
115
+ # The guarded parent samples this PID before sending stdin. No parser
116
+ # import or PDF decoding starts until that parent closes the pipe.
117
+ result = _extract(sys.stdin.buffer.read(MAX_DOCUMENT_BYTES + 1))
118
+ result.update(_memory_metadata(memory_control))
119
+ sys.stdout.buffer.write(json.dumps(result, ensure_ascii=True).encode())
120
+
121
+
122
+ def _memory_metadata(control):
123
+ return {'memory_control': control, 'memory_limit_bytes': MAX_MEMORY_BYTES,
124
+ 'memory_sample_interval_ms': MEMORY_SAMPLE_INTERVAL_MS if control == 'sampled_rss' else None,
125
+ 'hard_limit': control == 'address_space'}
126
+
127
+
128
+ class _MemoryGuardError(RuntimeError):
129
+ pass
130
+
131
+
132
+ def _sample_rss(target):
133
+ import psutil
134
+ try:
135
+ if not target.is_running():
136
+ raise psutil.NoSuchProcess(target.pid)
137
+ rss = target.memory_info().rss
138
+ if type(rss) is not int or rss < 0:
139
+ raise ValueError('invalid RSS')
140
+ except psutil.NoSuchProcess as exc:
141
+ raise _MemoryGuardError('parser_memory_monitor_exited') from exc
142
+ except Exception as exc:
143
+ raise _MemoryGuardError('parser_memory_monitor_failed') from exc
144
+ if rss > MAX_MEMORY_BYTES:
145
+ raise _MemoryGuardError('parser_memory_limit')
146
+
147
+
148
+ async def _watch_rss(process, target):
149
+ # Best-effort resident memory samples; scheduling and allocations can
150
+ # exceed this interval/threshold. This is not an OS memory allocation cap.
151
+ while process.returncode is None:
152
+ try:
153
+ _sample_rss(target)
154
+ except _MemoryGuardError as exc:
155
+ if str(exc) == 'parser_memory_monitor_exited':
156
+ await asyncio.sleep(0) # Let a genuine exit notification settle.
157
+ if process.returncode is not None:
158
+ return
159
+ raise
160
+ await asyncio.sleep(MEMORY_SAMPLE_INTERVAL_MS / 1000)
161
+
162
+
163
+ async def extract_document(data):
164
+ """No model, OCR, fetch, embedded action, or file reference is executed."""
165
+ guarded = sys.platform == 'darwin'
166
+ if guarded:
167
+ try:
168
+ import psutil
169
+ except ImportError:
170
+ return disposition('unsupported', 'parser_memory_monitor_unavailable')
171
+ process = await asyncio.create_subprocess_exec(
172
+ sys.executable, '-I', str(Path(__file__).resolve()),
173
+ *(['--rss-guarded'] if guarded else []),
174
+ stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE,
175
+ stderr=asyncio.subprocess.DEVNULL)
176
+ communication = watcher = target = None
177
+ guard_metadata = {}
178
+ try:
179
+ async with asyncio.timeout(MAX_PARSE_SECONDS):
180
+ if guarded:
181
+ try:
182
+ target = psutil.Process(process.pid)
183
+ except Exception as exc:
184
+ raise _MemoryGuardError('parser_memory_monitor_unavailable') from exc
185
+ guard_metadata = _memory_metadata('sampled_rss')
186
+ _sample_rss(target) # Must succeed before any PDF bytes go in.
187
+ watcher = asyncio.create_task(_watch_rss(process, target))
188
+ communication = asyncio.create_task(process.communicate(data))
189
+ if watcher is not None:
190
+ await asyncio.wait((communication, watcher), return_when=asyncio.FIRST_COMPLETED)
191
+ if watcher.done():
192
+ watcher.result() # A breach takes precedence over output.
193
+ output, _ = await asyncio.shield(communication)
194
+ if process.returncode:
195
+ return disposition('failed', 'parser_process_failed_or_resource_limit') | guard_metadata
196
+ if len(output) > MAX_RESULT_BYTES:
197
+ return disposition('failed', 'parser_output_exceeds_limit') | guard_metadata
198
+ result = json.loads(output)
199
+ if result.get('version') != VERSION or result.get('status') not in {'complete', 'partial', 'unsupported', 'failed'}:
200
+ raise ValueError('invalid parser result')
201
+ return result
202
+ except asyncio.TimeoutError:
203
+ return disposition('failed', 'parser_time_limit') | guard_metadata
204
+ except _MemoryGuardError as exc:
205
+ reason = 'parser_memory_monitor_failed' if str(exc) == 'parser_memory_monitor_exited' else str(exc)
206
+ return disposition('unsupported', reason) | guard_metadata
207
+ except (ValueError, TypeError):
208
+ return disposition('failed', 'invalid_parser_result') | guard_metadata
209
+ finally:
210
+ try:
211
+ if process.returncode is None:
212
+ try:
213
+ if target is None:
214
+ process.kill()
215
+ else:
216
+ try:
217
+ target.kill() # Checks this bound PID's identity.
218
+ except psutil.NoSuchProcess:
219
+ pass
220
+ except psutil.Error:
221
+ process.kill() # Owned-child Popen also polls first.
222
+ except ProcessLookupError:
223
+ pass
224
+ finally:
225
+ if watcher is not None:
226
+ watcher.cancel()
227
+ if communication is None:
228
+ communication = asyncio.create_task(process.communicate())
229
+ # Drain pipes while reaping, including cancellation during output.
230
+ await asyncio.gather(communication, *([watcher] if watcher is not None else []), return_exceptions=True)
231
+ await process.wait()
232
+
233
+
234
+ if __name__ == '__main__':
235
+ _child()