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/agents/store.py ADDED
@@ -0,0 +1,861 @@
1
+ """Agent and Invite stores for multi-agent Colony.
2
+
3
+ Provides:
4
+ - AgentStore: Registry of connected agents with SQLite persistence
5
+ - InviteStore: Setup code management with rate limiting
6
+ - Audit logging
7
+ - Certificate Revocation List (CRL)
8
+ """
9
+
10
+ import hashlib
11
+ import json
12
+ import logging
13
+ import os
14
+ import secrets
15
+ import shutil
16
+ import sqlite3
17
+ import time
18
+ import uuid
19
+ from contextlib import contextmanager
20
+ from datetime import datetime, timedelta, timezone
21
+ from pathlib import Path
22
+ from typing import Any, Dict, List, Optional
23
+
24
+ from .models import Agent, AgentMetadata, AgentStatus
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ # Type alias for Colony key manager (avoid circular import)
29
+ LocalKeyManager = Any
30
+
31
+
32
+ def get_state_dir() -> Path:
33
+ """Get Colony state directory."""
34
+ state_dir = os.environ.get("COLONY_STATE_DIR")
35
+ if state_dir:
36
+ return Path(state_dir)
37
+ return Path.home() / ".colony" / "data"
38
+
39
+
40
+ def generate_setup_code() -> str:
41
+ """Generate a random setup code: COLONY-XXXX-XXXX-XXXX."""
42
+ chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" # No 0/O, 1/I confusion
43
+ segments = ["COLONY"]
44
+ for _ in range(3):
45
+ segment = "".join(secrets.choice(chars) for _ in range(4))
46
+ segments.append(segment)
47
+ return "-".join(segments)
48
+
49
+
50
+ def hash_setup_code(code: str) -> str:
51
+ """Hash setup code for secure storage."""
52
+ pepper = os.environ.get(
53
+ "COLONY_CODE_PEPPER",
54
+ "default-pepper-change-in-production",
55
+ )
56
+ return hashlib.sha256(f"{code}:{pepper}".encode()).hexdigest()
57
+
58
+
59
+ class AgentStore:
60
+ """Manages agent registry with SQLite persistence and CRL support."""
61
+
62
+ def __init__(
63
+ self,
64
+ state_dir: Optional[Path] = None,
65
+ colony_key_manager: Optional[LocalKeyManager] = None,
66
+ ):
67
+ self._state_dir = Path(state_dir) if state_dir else get_state_dir()
68
+ self._state_dir.mkdir(parents=True, exist_ok=True)
69
+ self._db_path = self._state_dir / "agents.db"
70
+ self._backup_path = self._state_dir / "agents.db.backup"
71
+ self._colony_km = colony_key_manager
72
+
73
+ # In-memory CRL for fast lookup
74
+ self._revoked_node_ids: set = set()
75
+
76
+ self._db = self._init_db()
77
+ self._load_crl()
78
+
79
+ def _init_db(self) -> sqlite3.Connection:
80
+ """Initialize database with recovery."""
81
+ try:
82
+ return self._connect()
83
+ except sqlite3.DatabaseError:
84
+ logger.warning("agents.db corrupted, attempting recovery")
85
+
86
+ if self._backup_path.exists():
87
+ shutil.copy(self._backup_path, self._db_path)
88
+ logger.info("Restored agents.db from backup")
89
+ else:
90
+ self._db_path.unlink(missing_ok=True)
91
+ logger.warning("No backup available, starting fresh")
92
+
93
+ return self._connect()
94
+
95
+ def _connect(self) -> sqlite3.Connection:
96
+ """Connect to database with WAL mode for reliability."""
97
+ # check_same_thread=False allows TestClient to access the DB from
98
+ # a different thread (test thread vs event loop thread). This is
99
+ # safe for tests; production uses a single process/thread.
100
+ conn = sqlite3.connect(self._db_path, check_same_thread=False)
101
+ conn.row_factory = sqlite3.Row
102
+
103
+ # WAL mode for better crash recovery
104
+ conn.execute("PRAGMA journal_mode=WAL")
105
+ conn.execute("PRAGMA synchronous=NORMAL")
106
+ conn.execute("PRAGMA busy_timeout=5000")
107
+
108
+ self._create_tables(conn)
109
+ self._create_audit_tables(conn)
110
+
111
+ return conn
112
+
113
+ def _create_tables(self, conn: sqlite3.Connection) -> None:
114
+ """Create agents table."""
115
+ conn.execute(
116
+ """
117
+ CREATE TABLE IF NOT EXISTS agents (
118
+ agent_id TEXT PRIMARY KEY,
119
+ node_id TEXT NOT NULL,
120
+ colony_id TEXT NOT NULL,
121
+ name TEXT NOT NULL,
122
+
123
+ connection_mode TEXT DEFAULT 'local',
124
+ gateway_url TEXT,
125
+ websocket_connected INTEGER DEFAULT 0,
126
+
127
+ capabilities TEXT DEFAULT '[]',
128
+ is_primary INTEGER DEFAULT 0,
129
+ priority INTEGER DEFAULT 1,
130
+ max_concurrent INTEGER DEFAULT 5,
131
+ max_initiatives_per_hour INTEGER DEFAULT 10,
132
+ excluded_types TEXT DEFAULT '[]',
133
+ included_types TEXT DEFAULT '[]',
134
+
135
+ status TEXT DEFAULT 'offline',
136
+ current_assignments INTEGER DEFAULT 0,
137
+ last_seen_at TIMESTAMP,
138
+
139
+ metadata TEXT DEFAULT '{}',
140
+ registered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
141
+ node_cert TEXT,
142
+
143
+ UNIQUE(node_id, colony_id)
144
+ )
145
+ """
146
+ )
147
+
148
+ # Indexes
149
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(status)")
150
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_agents_primary ON agents(is_primary)")
151
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_agents_colony ON agents(colony_id)")
152
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_agents_last_seen ON agents(last_seen_at)")
153
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_agents_node_id ON agents(node_id)")
154
+
155
+ conn.commit()
156
+
157
+ def _create_audit_tables(self, conn: sqlite3.Connection) -> None:
158
+ """Create audit log table."""
159
+ conn.execute(
160
+ """
161
+ CREATE TABLE IF NOT EXISTS audit_log (
162
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
163
+ timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
164
+ action TEXT NOT NULL,
165
+ actor TEXT,
166
+ target TEXT,
167
+ details TEXT,
168
+ ip_address TEXT,
169
+ user_agent TEXT
170
+ )
171
+ """
172
+ )
173
+ conn.execute(
174
+ "CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_log(timestamp DESC)"
175
+ )
176
+ conn.execute(
177
+ "CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_log(action)"
178
+ )
179
+ conn.execute(
180
+ "CREATE INDEX IF NOT EXISTS idx_audit_actor ON audit_log(actor)"
181
+ )
182
+ conn.commit()
183
+
184
+ def _load_crl(self) -> None:
185
+ """Load CRL from database into memory."""
186
+ cursor = self._db.execute(
187
+ "SELECT node_id FROM agents WHERE status = ?",
188
+ [AgentStatus.REVOKED.value],
189
+ )
190
+ self._revoked_node_ids = {row["node_id"] for row in cursor.fetchall()}
191
+ logger.info("Loaded CRL: %d revoked node_ids", len(self._revoked_node_ids))
192
+
193
+ # ------------------------------------------------------------------
194
+ # Agent CRUD
195
+ # ------------------------------------------------------------------
196
+
197
+ def create(self, data: Dict[str, Any]) -> Agent:
198
+ """Create a new agent."""
199
+ agent_id = data.get("agent_id") or str(uuid.uuid4())
200
+ now = datetime.now(timezone.utc).isoformat()
201
+
202
+ cursor = self._db.execute(
203
+ """
204
+ INSERT INTO agents (
205
+ agent_id, node_id, colony_id, name,
206
+ connection_mode, gateway_url,
207
+ capabilities, is_primary, priority, max_concurrent, max_initiatives_per_hour,
208
+ excluded_types, included_types,
209
+ status, current_assignments, last_seen_at,
210
+ metadata, registered_at, node_cert
211
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
212
+ """,
213
+ [
214
+ agent_id,
215
+ data["node_id"],
216
+ data["colony_id"],
217
+ data["name"],
218
+ data.get("connection_mode", "local"),
219
+ data.get("gateway_url"),
220
+ json.dumps(data.get("capabilities", [])),
221
+ 1 if data.get("is_primary") else 0,
222
+ data.get("priority", 1),
223
+ data.get("max_concurrent", 5),
224
+ data.get("max_initiatives_per_hour", 10),
225
+ json.dumps(data.get("excluded_types", [])),
226
+ json.dumps(data.get("included_types", [])),
227
+ data.get("status", "offline"),
228
+ data.get("current_assignments", 0),
229
+ data.get("last_seen_at"),
230
+ json.dumps(data.get("metadata", {})),
231
+ now,
232
+ json.dumps(data["node_cert"]) if data.get("node_cert") else None,
233
+ ],
234
+ )
235
+ self._db.commit()
236
+
237
+ # Log audit
238
+ self.log_audit(
239
+ action="agent_create",
240
+ actor="system",
241
+ target=agent_id,
242
+ details={"name": data["name"], "node_id": data["node_id"]},
243
+ )
244
+
245
+ return self.get(agent_id)
246
+
247
+ def get(self, agent_id: str) -> Optional[Agent]:
248
+ """Get agent by ID."""
249
+ cursor = self._db.execute(
250
+ "SELECT * FROM agents WHERE agent_id = ?",
251
+ [agent_id],
252
+ )
253
+ row = cursor.fetchone()
254
+ if row:
255
+ return Agent.from_row(dict(row))
256
+ return None
257
+
258
+ def get_by_node_id(self, node_id: str) -> Optional[Agent]:
259
+ """Get agent by node ID."""
260
+ cursor = self._db.execute(
261
+ "SELECT * FROM agents WHERE node_id = ?",
262
+ [node_id],
263
+ )
264
+ row = cursor.fetchone()
265
+ if row:
266
+ return Agent.from_row(dict(row))
267
+ return None
268
+
269
+ def list(
270
+ self,
271
+ status: Optional[List[str]] = None,
272
+ colony_id: Optional[str] = None,
273
+ is_primary: Optional[bool] = None,
274
+ capability: Optional[str] = None,
275
+ limit: int = 100,
276
+ offset: int = 0,
277
+ ) -> List[Agent]:
278
+ """List agents with filters."""
279
+ query = "SELECT * FROM agents WHERE 1=1"
280
+ params: List[Any] = []
281
+
282
+ if status:
283
+ placeholders = ",".join("?" * len(status))
284
+ query += f" AND status IN ({placeholders})"
285
+ params.extend(status)
286
+
287
+ if colony_id:
288
+ query += " AND colony_id = ?"
289
+ params.append(colony_id)
290
+
291
+ if is_primary is not None:
292
+ query += " AND is_primary = ?"
293
+ params.append(1 if is_primary else 0)
294
+
295
+ query += " ORDER BY priority DESC, name ASC LIMIT ? OFFSET ?"
296
+ params.extend([limit, offset])
297
+
298
+ cursor = self._db.execute(query, params)
299
+ agents = [Agent.from_row(dict(row)) for row in cursor.fetchall()]
300
+
301
+ if capability:
302
+ agents = [a for a in agents if capability in a.capabilities]
303
+
304
+ return agents
305
+
306
+ def update(self, agent_id: str, **updates) -> Optional[Agent]:
307
+ """Update agent fields."""
308
+ if not updates:
309
+ return self.get(agent_id)
310
+
311
+ # Build SET clause
312
+ set_parts = []
313
+ params = []
314
+
315
+ for key, value in updates.items():
316
+ if key in (
317
+ "capabilities",
318
+ "excluded_types",
319
+ "included_types",
320
+ "metadata",
321
+ "node_cert",
322
+ ):
323
+ set_parts.append(f"{key} = ?")
324
+ params.append(json.dumps(value) if not isinstance(value, str) else value)
325
+ elif key in ("is_primary", "websocket_connected"):
326
+ set_parts.append(f"{key} = ?")
327
+ params.append(1 if value else 0)
328
+ elif key in ("last_seen_at",) and isinstance(value, datetime):
329
+ set_parts.append(f"{key} = ?")
330
+ params.append(value.isoformat())
331
+ else:
332
+ set_parts.append(f"{key} = ?")
333
+ params.append(value)
334
+
335
+ if not set_parts:
336
+ return self.get(agent_id)
337
+
338
+ params.append(agent_id)
339
+ query = f"UPDATE agents SET {', '.join(set_parts)} WHERE agent_id = ?"
340
+
341
+ self._db.execute(query, params)
342
+ self._db.commit()
343
+
344
+ return self.get(agent_id)
345
+
346
+ def delete(self, agent_id: str) -> bool:
347
+ """Delete an agent."""
348
+ cursor = self._db.execute(
349
+ "DELETE FROM agents WHERE agent_id = ?",
350
+ [agent_id],
351
+ )
352
+ self._db.commit()
353
+ return cursor.rowcount > 0
354
+
355
+ # ------------------------------------------------------------------
356
+ # Status Management
357
+ # ------------------------------------------------------------------
358
+
359
+ def set_online(
360
+ self,
361
+ agent_id: str,
362
+ websocket_connected: bool = False,
363
+ metadata: Optional[Dict[str, Any]] = None,
364
+ ) -> Optional[Agent]:
365
+ """Mark agent as online."""
366
+ updates = {
367
+ "status": AgentStatus.ONLINE.value,
368
+ "last_seen_at": datetime.now(timezone.utc),
369
+ "websocket_connected": websocket_connected,
370
+ }
371
+ if metadata:
372
+ updates["metadata"] = metadata
373
+
374
+ return self.update(agent_id, **updates)
375
+
376
+ def set_offline(self, agent_id: str) -> Optional[Agent]:
377
+ """Mark agent as offline."""
378
+ return self.update(
379
+ agent_id,
380
+ status=AgentStatus.OFFLINE.value,
381
+ websocket_connected=False,
382
+ )
383
+
384
+ def mark_all_offline(self) -> int:
385
+ """Mark all agents as offline (called on Colony restart)."""
386
+ cursor = self._db.execute(
387
+ "UPDATE agents SET status = ?, websocket_connected = 0",
388
+ [AgentStatus.OFFLINE.value],
389
+ )
390
+ self._db.commit()
391
+ return cursor.rowcount
392
+
393
+ # ------------------------------------------------------------------
394
+ # Revocation
395
+ # ------------------------------------------------------------------
396
+
397
+ def revoke(self, agent_id: str, reason: str = "") -> Optional[Agent]:
398
+ """Revoke an agent."""
399
+ agent = self.get(agent_id)
400
+ if not agent:
401
+ return None
402
+
403
+ # Update status
404
+ self.update(agent_id, status=AgentStatus.REVOKED.value)
405
+
406
+ # Add to CRL
407
+ self._revoked_node_ids.add(agent.node_id)
408
+
409
+ # Log audit
410
+ self.log_audit(
411
+ action="agent_revoke",
412
+ actor="api",
413
+ target=agent_id,
414
+ details={"reason": reason, "node_id": agent.node_id},
415
+ )
416
+
417
+ return self.get(agent_id)
418
+
419
+ def is_revoked(self, node_id: str) -> bool:
420
+ """Check if node_id is revoked."""
421
+ return node_id in self._revoked_node_ids
422
+
423
+ # ------------------------------------------------------------------
424
+ # Assignment Tracking
425
+ # ------------------------------------------------------------------
426
+
427
+ def increment_assignments(self, agent_id: str) -> None:
428
+ """Increment current_assignments counter."""
429
+ self._db.execute(
430
+ "UPDATE agents SET current_assignments = current_assignments + 1 WHERE agent_id = ?",
431
+ [agent_id],
432
+ )
433
+ self._db.commit()
434
+
435
+ def decrement_assignments(self, agent_id: str) -> None:
436
+ """Decrement current_assignments counter."""
437
+ self._db.execute(
438
+ "UPDATE agents SET current_assignments = MAX(0, current_assignments - 1) WHERE agent_id = ?",
439
+ [agent_id],
440
+ )
441
+ self._db.commit()
442
+
443
+ # ------------------------------------------------------------------
444
+ # Ghost Cleanup
445
+ # ------------------------------------------------------------------
446
+
447
+ def list_ghosts(self, registered_before: datetime) -> List[Agent]:
448
+ """List agents that registered but never connected.
449
+
450
+ Ghost agents are:
451
+ - status='offline'
452
+ - websocket_connected=0
453
+ - last_seen_at IS NULL (never connected)
454
+ - registered_at < threshold
455
+ """
456
+ cursor = self._db.execute(
457
+ """
458
+ SELECT * FROM agents
459
+ WHERE status = ?
460
+ AND websocket_connected = 0
461
+ AND last_seen_at IS NULL
462
+ AND registered_at < ?
463
+ """,
464
+ [AgentStatus.OFFLINE.value, registered_before.isoformat()],
465
+ )
466
+ return [Agent.from_row(dict(row)) for row in cursor.fetchall()]
467
+
468
+ # ------------------------------------------------------------------
469
+ # Audit Logging
470
+ # ------------------------------------------------------------------
471
+
472
+ def log_audit(
473
+ self,
474
+ action: str,
475
+ actor: str,
476
+ target: str,
477
+ details: Optional[Dict[str, Any]] = None,
478
+ ip_address: Optional[str] = None,
479
+ user_agent: Optional[str] = None,
480
+ ) -> None:
481
+ """Log audit event."""
482
+ self._db.execute(
483
+ """
484
+ INSERT INTO audit_log (action, actor, target, details, ip_address, user_agent)
485
+ VALUES (?, ?, ?, ?, ?, ?)
486
+ """,
487
+ [
488
+ action,
489
+ actor,
490
+ target,
491
+ json.dumps(details) if details else None,
492
+ ip_address,
493
+ user_agent,
494
+ ],
495
+ )
496
+ self._db.commit()
497
+
498
+ def get_audit_logs(
499
+ self,
500
+ action: Optional[str] = None,
501
+ actor: Optional[str] = None,
502
+ since: Optional[datetime] = None,
503
+ limit: int = 100,
504
+ ) -> List[Dict[str, Any]]:
505
+ """Get audit logs with filters."""
506
+ query = "SELECT * FROM audit_log WHERE 1=1"
507
+ params: List[Any] = []
508
+
509
+ if action:
510
+ query += " AND action = ?"
511
+ params.append(action)
512
+
513
+ if actor:
514
+ query += " AND actor = ?"
515
+ params.append(actor)
516
+
517
+ if since:
518
+ query += " AND timestamp >= ?"
519
+ params.append(since.isoformat())
520
+
521
+ query += " ORDER BY timestamp DESC LIMIT ?"
522
+ params.append(limit)
523
+
524
+ cursor = self._db.execute(query, params)
525
+ return [dict(row) for row in cursor.fetchall()]
526
+
527
+ # ------------------------------------------------------------------
528
+ # Certificate Signing
529
+ # ------------------------------------------------------------------
530
+
531
+ async def sign_node_certificate(
532
+ self,
533
+ node_id: str,
534
+ node_public_key: str,
535
+ expires_days: int = 365,
536
+ ) -> Dict[str, Any]:
537
+ """Sign a node certificate for remote agent."""
538
+ if not self._colony_km:
539
+ raise ValueError("Colony key not available for signing")
540
+
541
+ # Import here to avoid circular dependency
542
+ from apsimo.chain.identity import get_or_create_colony_id
543
+
544
+ colony_id = get_or_create_colony_id(self._state_dir)
545
+
546
+ now = datetime.now(timezone.utc)
547
+ expires_at = now + timedelta(days=expires_days)
548
+
549
+ cert = {
550
+ "colony_id": colony_id,
551
+ "node_id": node_id,
552
+ "node_public_key_ed25519": node_public_key,
553
+ "issued_at": now.isoformat(),
554
+ "expires_at": expires_at.isoformat(),
555
+ }
556
+
557
+ # Sign with Colony private key
558
+ # The LocalKeyManager should have a sign() method
559
+ payload = json.dumps(cert, sort_keys=True).encode()
560
+ signature = self._colony_km.sign(payload)
561
+ cert["signature"] = signature.hex()
562
+
563
+ return cert
564
+
565
+ # ------------------------------------------------------------------
566
+ # Backup/Recovery
567
+ # ------------------------------------------------------------------
568
+
569
+ def backup(self) -> None:
570
+ """Create backup of database."""
571
+ shutil.copy2(self._db_path, self._backup_path)
572
+
573
+ def close(self) -> None:
574
+ """Close connection and create backup."""
575
+ self.backup()
576
+ self._db.close()
577
+
578
+
579
+ class InviteStore:
580
+ """Manages agent invitation/setup codes."""
581
+
582
+ MAX_FAILED_ATTEMPTS = 5
583
+ LOCKOUT_MINUTES = 15
584
+ DEFAULT_EXPIRY_SECONDS = 900 # 15 minutes
585
+
586
+ def __init__(self, state_dir: Optional[Path] = None):
587
+ self._state_dir = Path(state_dir) if state_dir else get_state_dir()
588
+ self._state_dir.mkdir(parents=True, exist_ok=True)
589
+ self._db_path = self._state_dir / "agents.db"
590
+ self._db = self._get_or_create_db()
591
+
592
+ def _get_or_create_db(self) -> sqlite3.Connection:
593
+ """Get or create database connection."""
594
+ # check_same_thread=False allows TestClient to access the DB from
595
+ # a different thread (test thread vs event loop thread).
596
+ conn = sqlite3.connect(self._db_path, check_same_thread=False)
597
+ conn.row_factory = sqlite3.Row
598
+ conn.execute("PRAGMA journal_mode=WAL")
599
+ conn.execute("PRAGMA synchronous=NORMAL")
600
+ self._create_tables(conn)
601
+ return conn
602
+
603
+ def _create_tables(self, conn: sqlite3.Connection) -> None:
604
+ """Create invites table."""
605
+ conn.execute(
606
+ """
607
+ CREATE TABLE IF NOT EXISTS agent_invites (
608
+ code TEXT,
609
+ code_hash TEXT UNIQUE,
610
+ colony_id TEXT NOT NULL,
611
+
612
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
613
+ expires_at TIMESTAMP NOT NULL,
614
+ max_uses INTEGER DEFAULT 1,
615
+ use_count INTEGER DEFAULT 0,
616
+
617
+ failed_attempts INTEGER DEFAULT 0,
618
+ locked_until TIMESTAMP,
619
+
620
+ used_at TIMESTAMP,
621
+ used_by_agent_id TEXT,
622
+ used_by_node_id TEXT,
623
+
624
+ granted_capabilities TEXT DEFAULT '[]',
625
+ granted_is_primary INTEGER DEFAULT 0,
626
+ granted_max_concurrent INTEGER DEFAULT 5,
627
+
628
+ created_by_agent_id TEXT,
629
+ label TEXT
630
+ )
631
+ """
632
+ )
633
+
634
+ conn.execute(
635
+ "CREATE INDEX IF NOT EXISTS idx_invites_expires ON agent_invites(expires_at)"
636
+ )
637
+ conn.execute(
638
+ "CREATE INDEX IF NOT EXISTS idx_invites_code_hash ON agent_invites(code_hash)"
639
+ )
640
+ conn.execute(
641
+ "CREATE INDEX IF NOT EXISTS idx_invites_locked ON agent_invites(locked_until)"
642
+ )
643
+ conn.commit()
644
+
645
+ def create(
646
+ self,
647
+ colony_id: str,
648
+ capabilities: Optional[List[str]] = None,
649
+ is_primary: bool = False,
650
+ max_concurrent: int = 5,
651
+ expires_seconds: Optional[int] = None,
652
+ label: Optional[str] = None,
653
+ created_by_agent_id: Optional[str] = None,
654
+ ) -> Dict[str, Any]:
655
+ """Create a new invite."""
656
+ code = generate_setup_code()
657
+ code_hash = hash_setup_code(code)
658
+
659
+ now = datetime.now(timezone.utc)
660
+ expires_at = now + timedelta(seconds=expires_seconds or self.DEFAULT_EXPIRY_SECONDS)
661
+
662
+ self._db.execute(
663
+ """
664
+ INSERT INTO agent_invites (
665
+ code, code_hash, colony_id,
666
+ expires_at,
667
+ granted_capabilities, granted_is_primary, granted_max_concurrent,
668
+ label, created_by_agent_id
669
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
670
+ """,
671
+ [
672
+ code, # Keep for display, but lookups use hash
673
+ code_hash,
674
+ colony_id,
675
+ expires_at.isoformat(),
676
+ json.dumps(capabilities or []),
677
+ 1 if is_primary else 0,
678
+ max_concurrent,
679
+ label,
680
+ created_by_agent_id,
681
+ ],
682
+ )
683
+ self._db.commit()
684
+
685
+ return {
686
+ "setup_code": code, # Return plaintext once
687
+ "code_hash": code_hash,
688
+ "expires_at": expires_at.isoformat(),
689
+ "expires_in_seconds": expires_seconds or self.DEFAULT_EXPIRY_SECONDS,
690
+ "capabilities": capabilities or [],
691
+ "is_primary": is_primary,
692
+ "max_concurrent": max_concurrent,
693
+ }
694
+
695
+ def validate(self, code: str) -> Dict[str, Any]:
696
+ """Validate setup code (checking rate limits).
697
+
698
+ Returns invite data if valid.
699
+ Raises ValueError if invalid, expired, used, or locked.
700
+ """
701
+ code_hash = hash_setup_code(code)
702
+ now = datetime.now(timezone.utc)
703
+
704
+ cursor = self._db.execute(
705
+ "SELECT * FROM agent_invites WHERE code_hash = ?",
706
+ [code_hash],
707
+ )
708
+ invite = cursor.fetchone()
709
+
710
+ if not invite:
711
+ raise ValueError("Invalid setup code")
712
+
713
+ invite = dict(invite)
714
+
715
+ # Check if locked
716
+ if invite.get("locked_until"):
717
+ locked_until = datetime.fromisoformat(invite["locked_until"])
718
+ if now < locked_until:
719
+ raise ValueError(f"Setup code locked until {locked_until.isoformat()}")
720
+
721
+ # Check expiry
722
+ expires_at = datetime.fromisoformat(invite["expires_at"])
723
+ if now > expires_at:
724
+ raise ValueError("Setup code expired")
725
+
726
+ # Check usage
727
+ if invite["use_count"] >= invite["max_uses"]:
728
+ raise ValueError("Setup code already used")
729
+
730
+ return invite
731
+
732
+ def record_failed_attempt(self, code: str) -> None:
733
+ """Record failed validation attempt and check lockout."""
734
+ code_hash = hash_setup_code(code)
735
+ now = datetime.now(timezone.utc)
736
+
737
+ # Increment failed attempts
738
+ cursor = self._db.execute(
739
+ "UPDATE agent_invites SET failed_attempts = failed_attempts + 1 WHERE code_hash = ?",
740
+ [code_hash],
741
+ )
742
+
743
+ if cursor.rowcount == 0:
744
+ return
745
+
746
+ # Check if we should lock
747
+ cursor = self._db.execute(
748
+ "SELECT failed_attempts FROM agent_invites WHERE code_hash = ?",
749
+ [code_hash],
750
+ )
751
+ row = cursor.fetchone()
752
+ if row and row["failed_attempts"] >= self.MAX_FAILED_ATTEMPTS:
753
+ locked_until = now + timedelta(minutes=self.LOCKOUT_MINUTES)
754
+ self._db.execute(
755
+ "UPDATE agent_invites SET locked_until = ? WHERE code_hash = ?",
756
+ [locked_until.isoformat(), code_hash],
757
+ )
758
+ logger.warning("Setup code locked due to %d failed attempts", row["failed_attempts"])
759
+
760
+ self._db.commit()
761
+
762
+ def clear_failed_attempts(self, code: str) -> None:
763
+ """Clear failed attempts after successful use."""
764
+ code_hash = hash_setup_code(code)
765
+ self._db.execute(
766
+ "UPDATE agent_invites SET failed_attempts = 0 WHERE code_hash = ?",
767
+ [code_hash],
768
+ )
769
+ self._db.commit()
770
+
771
+ def use(
772
+ self,
773
+ code: str,
774
+ node_id: str,
775
+ agent_id: str,
776
+ ) -> Dict[str, Any]:
777
+ """Use setup code (atomic operation).
778
+
779
+ This validates, marks as used, and returns the invite data.
780
+ Raises ValueError if already used or invalid.
781
+ """
782
+ code_hash = hash_setup_code(code)
783
+ now = datetime.now(timezone.utc)
784
+
785
+ # Atomic UPDATE with WHERE conditions
786
+ cursor = self._db.execute(
787
+ """
788
+ UPDATE agent_invites
789
+ SET used_at = ?,
790
+ used_by_node_id = ?,
791
+ used_by_agent_id = ?,
792
+ use_count = use_count + 1
793
+ WHERE code_hash = ?
794
+ AND used_at IS NULL
795
+ AND expires_at > ?
796
+ AND (locked_until IS NULL OR locked_until < ?)
797
+ """,
798
+ [
799
+ now.isoformat(),
800
+ node_id,
801
+ agent_id,
802
+ code_hash,
803
+ now.isoformat(),
804
+ now.isoformat(),
805
+ ],
806
+ )
807
+ self._db.commit()
808
+
809
+ if cursor.rowcount == 0:
810
+ # Either already used, expired, or locked
811
+ raise ValueError("Setup code already used, expired, or locked")
812
+
813
+ # Get the invite
814
+ cursor = self._db.execute(
815
+ "SELECT * FROM agent_invites WHERE code_hash = ?",
816
+ [code_hash],
817
+ )
818
+ return dict(cursor.fetchone())
819
+
820
+ def get(self, code: str) -> Optional[Dict[str, Any]]:
821
+ """Get invite by code."""
822
+ code_hash = hash_setup_code(code)
823
+ cursor = self._db.execute(
824
+ "SELECT * FROM agent_invites WHERE code_hash = ?",
825
+ [code_hash],
826
+ )
827
+ row = cursor.fetchone()
828
+ return dict(row) if row else None
829
+
830
+ def list(
831
+ self,
832
+ colony_id: Optional[str] = None,
833
+ unused_only: bool = False,
834
+ limit: int = 50,
835
+ ) -> List[Dict[str, Any]]:
836
+ """List invites."""
837
+ query = "SELECT * FROM agent_invites WHERE 1=1"
838
+ params: List[Any] = []
839
+
840
+ if colony_id:
841
+ query += " AND colony_id = ?"
842
+ params.append(colony_id)
843
+
844
+ if unused_only:
845
+ query += " AND used_at IS NULL"
846
+
847
+ query += " ORDER BY created_at DESC LIMIT ?"
848
+ params.append(limit)
849
+
850
+ cursor = self._db.execute(query, params)
851
+ return [dict(row) for row in cursor.fetchall()]
852
+
853
+ def delete(self, code: str) -> bool:
854
+ """Delete an invite."""
855
+ code_hash = hash_setup_code(code)
856
+ cursor = self._db.execute(
857
+ "DELETE FROM agent_invites WHERE code_hash = ?",
858
+ [code_hash],
859
+ )
860
+ self._db.commit()
861
+ return cursor.rowcount > 0