apsimo 1.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (614) hide show
  1. apsimo/__init__.py +38 -0
  2. apsimo/__main__.py +6 -0
  3. apsimo/agent/__init__.py +6 -0
  4. apsimo/agent/client.py +276 -0
  5. apsimo/agent/models.py +46 -0
  6. apsimo/agents/__init__.py +20 -0
  7. apsimo/agents/models.py +264 -0
  8. apsimo/agents/store.py +861 -0
  9. apsimo/agents/websocket.py +522 -0
  10. apsimo/api/__init__.py +1 -0
  11. apsimo/api/auth_telemetry.py +287 -0
  12. apsimo/api/authority.py +1203 -0
  13. apsimo/api/contact_grants.py +347 -0
  14. apsimo/api/middleware.py +483 -0
  15. apsimo/api/routers/__init__.py +1 -0
  16. apsimo/api/routers/commitment_work.py +265 -0
  17. apsimo/api/routers/context_gate.py +123 -0
  18. apsimo/api/routers/executions.py +140 -0
  19. apsimo/api/routers/followup_plans.py +147 -0
  20. apsimo/api/routers/governed_actions.py +162 -0
  21. apsimo/api/routers/host.py +14473 -0
  22. apsimo/api/routers/initiative_work.py +115 -0
  23. apsimo/api/routers/mining.py +104 -0
  24. apsimo/api/routers/observations.py +110 -0
  25. apsimo/api/routers/social_state.py +225 -0
  26. apsimo/api/routers/task_queue.py +2715 -0
  27. apsimo/api/routers/temporal_followups.py +251 -0
  28. apsimo/api/routers/transport.py +110 -0
  29. apsimo/api/routers/transport_ingress_api.py +240 -0
  30. apsimo/api/schemas/__init__.py +1 -0
  31. apsimo/api/schemas/host.py +1949 -0
  32. apsimo/autonomy/cli.py +110 -0
  33. apsimo/autonomy/condition_worker.py +437 -0
  34. apsimo/autonomy/config.py +424 -0
  35. apsimo/autonomy/loop.py +4316 -0
  36. apsimo/autonomy/registry.py +339 -0
  37. apsimo/autonomy/scheduler.py +1822 -0
  38. apsimo/autonomy/synthesis.py +449 -0
  39. apsimo/backup.py +962 -0
  40. apsimo/beliefs/__init__.py +23 -0
  41. apsimo/beliefs/contradictions.py +109 -0
  42. apsimo/beliefs/decay.py +61 -0
  43. apsimo/beliefs/engine.py +479 -0
  44. apsimo/beliefs/models.py +67 -0
  45. apsimo/beliefs/promotion.py +41 -0
  46. apsimo/beliefs/resolve.py +58 -0
  47. apsimo/beliefs/source_claims.py +690 -0
  48. apsimo/beliefs/source_projection.py +883 -0
  49. apsimo/beliefs/source_time.py +208 -0
  50. apsimo/beliefs/store.py +133 -0
  51. apsimo/briefings/aggregators.py +824 -0
  52. apsimo/briefings/composer.py +420 -0
  53. apsimo/briefings/config.py +55 -0
  54. apsimo/briefings/delivery.py +439 -0
  55. apsimo/briefings/engagement.py +97 -0
  56. apsimo/briefings/engine.py +274 -0
  57. apsimo/briefings/enhancer.py +99 -0
  58. apsimo/briefings/models.py +183 -0
  59. apsimo/briefings/scheduler.py +382 -0
  60. apsimo/briefings/store.py +435 -0
  61. apsimo/chain/__init__.py +48 -0
  62. apsimo/chain/block.py +100 -0
  63. apsimo/chain/cli.py +704 -0
  64. apsimo/chain/genesis.py +443 -0
  65. apsimo/chain/identity.py +416 -0
  66. apsimo/chain/keys.py +1025 -0
  67. apsimo/chain/local_keys.py +187 -0
  68. apsimo/chain/manager.py +290 -0
  69. apsimo/chain/node.py +163 -0
  70. apsimo/chain/plugin_transactions.py +371 -0
  71. apsimo/chain/protocol.py +220 -0
  72. apsimo/chain/state_machine.py +676 -0
  73. apsimo/chain/storage.py +503 -0
  74. apsimo/chain/transactions.py +250 -0
  75. apsimo/chain/validation.py +397 -0
  76. apsimo/channels/__init__.py +1 -0
  77. apsimo/channels/manifest.py +31 -0
  78. apsimo/channels/migrations/001_channels_schema.sql +12 -0
  79. apsimo/channels/phone_gateways.py +42 -0
  80. apsimo/channels/presence.py +188 -0
  81. apsimo/channels/router.py +235 -0
  82. apsimo/channels/store.py +231 -0
  83. apsimo/cli.py +2688 -0
  84. apsimo/cognition/__init__.py +11 -0
  85. apsimo/cognition/charter.py +398 -0
  86. apsimo/cognition/drive_governance.py +3530 -0
  87. apsimo/cognition/evidence_pipeline.py +1627 -0
  88. apsimo/cognition/external_events.py +932 -0
  89. apsimo/cognition/goal_spine.py +3488 -0
  90. apsimo/cognition/introspection.py +214 -0
  91. apsimo/cognition/prompt.py +150 -0
  92. apsimo/cognition/runtime.py +108 -0
  93. apsimo/cognition/trigger.py +154 -0
  94. apsimo/commitments/__init__.py +18 -0
  95. apsimo/commitments/local_work.py +355 -0
  96. apsimo/commitments/store.py +1052 -0
  97. apsimo/commitments/work.py +91 -0
  98. apsimo/compat.py +53 -0
  99. apsimo/compression/__init__.py +467 -0
  100. apsimo/connectors/__init__.py +21 -0
  101. apsimo/connectors/base.py +152 -0
  102. apsimo/connectors/caldav_calendar.py +125 -0
  103. apsimo/connectors/fs_documents.py +85 -0
  104. apsimo/connectors/imap_email.py +138 -0
  105. apsimo/connectors/manager.py +218 -0
  106. apsimo/connectors/webhook_pull.py +88 -0
  107. apsimo/contacts/__init__.py +33 -0
  108. apsimo/contacts/comms.py +357 -0
  109. apsimo/contacts/config.py +79 -0
  110. apsimo/contacts/exporters/__init__.py +1 -0
  111. apsimo/contacts/exporters/vcard.py +71 -0
  112. apsimo/contacts/identity_links.py +251 -0
  113. apsimo/contacts/importer.py +280 -0
  114. apsimo/contacts/importers/__init__.py +1 -0
  115. apsimo/contacts/importers/batch.py +43 -0
  116. apsimo/contacts/importers/macos_contacts.py +101 -0
  117. apsimo/contacts/migrations/001_contacts_schema.sql +141 -0
  118. apsimo/contacts/migrations/002_trust_scopes.sql +36 -0
  119. apsimo/contacts/migrations/003_open_gateway_enum.sql +32 -0
  120. apsimo/contacts/migrations/004_contact_provision_operations.sql +18 -0
  121. apsimo/contacts/migrations/005_identity_links.sql +27 -0
  122. apsimo/contacts/models.py +308 -0
  123. apsimo/contacts/scoring.py +16 -0
  124. apsimo/contacts/store.py +1623 -0
  125. apsimo/contacts/transport_ingress.py +252 -0
  126. apsimo/contacts/world_bridge.py +314 -0
  127. apsimo/contextgate/__init__.py +69 -0
  128. apsimo/contextgate/chunker.py +169 -0
  129. apsimo/contextgate/estimate.py +54 -0
  130. apsimo/contextgate/gate.py +313 -0
  131. apsimo/contextgate/retrieve.py +115 -0
  132. apsimo/delivery/__init__.py +16 -0
  133. apsimo/delivery/bridge.py +1260 -0
  134. apsimo/delivery/channels.py +526 -0
  135. apsimo/delivery/classification.py +50 -0
  136. apsimo/delivery/rate_limiter.py +268 -0
  137. apsimo/delivery/reachout_policy.py +206 -0
  138. apsimo/directed/__init__.py +22 -0
  139. apsimo/directed/audit.py +167 -0
  140. apsimo/directed/intake.py +95 -0
  141. apsimo/directed/models.py +191 -0
  142. apsimo/directed/service.py +509 -0
  143. apsimo/directives/__init__.py +25 -0
  144. apsimo/directives/evidence.py +87 -0
  145. apsimo/directives/extractor.py +188 -0
  146. apsimo/directives/guard.py +364 -0
  147. apsimo/directives/models.py +206 -0
  148. apsimo/directives/service.py +372 -0
  149. apsimo/directives/store.py +167 -0
  150. apsimo/doctor.py +2173 -0
  151. apsimo/environment.py +43 -0
  152. apsimo/events/__init__.py +33 -0
  153. apsimo/events/broadcaster.py +98 -0
  154. apsimo/events/bus.py +217 -0
  155. apsimo/events/journal.py +863 -0
  156. apsimo/events/stream.py +131 -0
  157. apsimo/events/types.py +150 -0
  158. apsimo/execution_results.py +357 -0
  159. apsimo/feedback/__init__.py +5 -0
  160. apsimo/feedback/store.py +76 -0
  161. apsimo/feeds/__init__.py +19 -0
  162. apsimo/feeds/cli.py +84 -0
  163. apsimo/feeds/engine.py +437 -0
  164. apsimo/feeds/example-feed.yaml +77 -0
  165. apsimo/feeds/hermes_cron.py +126 -0
  166. apsimo/feeds/manager.py +235 -0
  167. apsimo/feeds/spec.py +250 -0
  168. apsimo/feeds/template.py +202 -0
  169. apsimo/gate/__init__.py +18 -0
  170. apsimo/gate/audit.py +61 -0
  171. apsimo/gate/communication_policy.py +166 -0
  172. apsimo/gate/config.py +72 -0
  173. apsimo/gate/context_provenance.py +170 -0
  174. apsimo/gate/env_risk.py +226 -0
  175. apsimo/gate/guard_audit.py +353 -0
  176. apsimo/gate/layers/__init__.py +1 -0
  177. apsimo/gate/layers/base.py +15 -0
  178. apsimo/gate/layers/l1_recipient.py +66 -0
  179. apsimo/gate/layers/l2_pii.py +134 -0
  180. apsimo/gate/layers/l3_cross_context.py +50 -0
  181. apsimo/gate/layers/l4_trust_tier.py +78 -0
  182. apsimo/gate/layers/l5_injection.py +199 -0
  183. apsimo/gate/layers/l6_review.py +86 -0
  184. apsimo/gate/layers/l7_delay.py +100 -0
  185. apsimo/gate/layers/tom2_epistemic.py +185 -0
  186. apsimo/gate/models.py +64 -0
  187. apsimo/gate/pending_dispatch.py +5 -0
  188. apsimo/gate/pipeline.py +206 -0
  189. apsimo/gate/rejection.py +259 -0
  190. apsimo/gate/response_guard.py +700 -0
  191. apsimo/gate/rulesets/injection_v1.yaml +51 -0
  192. apsimo/gate/surface_policy.py +189 -0
  193. apsimo/gate/taint.py +226 -0
  194. apsimo/genesis.json +9 -0
  195. apsimo/goals/__init__.py +100 -0
  196. apsimo/goals/config.py +38 -0
  197. apsimo/goals/decomposer.py +421 -0
  198. apsimo/goals/engine.py +617 -0
  199. apsimo/goals/inference.py +354 -0
  200. apsimo/goals/models.py +302 -0
  201. apsimo/goals/priority.py +270 -0
  202. apsimo/goals/queue_bridge.py +149 -0
  203. apsimo/goals/replan.py +450 -0
  204. apsimo/goals/schema.sql +89 -0
  205. apsimo/goals/store.py +692 -0
  206. apsimo/governed_actions.py +1708 -0
  207. apsimo/harness_integration/__init__.py +45 -0
  208. apsimo/harness_integration/context.py +41 -0
  209. apsimo/harness_integration/skills.py +231 -0
  210. apsimo/identity/__init__.py +26 -0
  211. apsimo/identity/participants.py +181 -0
  212. apsimo/identity/resolver.py +329 -0
  213. apsimo/identity_bootstrap/__init__.py +5 -0
  214. apsimo/identity_bootstrap/builder.py +208 -0
  215. apsimo/identity_bootstrap/corpus.py +443 -0
  216. apsimo/identity_bootstrap/models.py +54 -0
  217. apsimo/identity_bootstrap/runner.py +353 -0
  218. apsimo/identity_bootstrap/seeders/__init__.py +25 -0
  219. apsimo/identity_bootstrap/seeders/briefings.py +109 -0
  220. apsimo/identity_bootstrap/seeders/chain.py +57 -0
  221. apsimo/identity_bootstrap/seeders/goals.py +128 -0
  222. apsimo/identity_bootstrap/seeders/memory.py +191 -0
  223. apsimo/identity_bootstrap/seeders/neo4j_cognition.py +79 -0
  224. apsimo/identity_bootstrap/seeders/relationship.py +152 -0
  225. apsimo/identity_bootstrap/seeders/sessions.py +67 -0
  226. apsimo/identity_bootstrap/seeders/skills.py +92 -0
  227. apsimo/identity_bootstrap/seeders/task_queue.py +72 -0
  228. apsimo/identity_bootstrap/seeders/world_model.py +143 -0
  229. apsimo/identity_bootstrap/self_query.py +92 -0
  230. apsimo/identity_bootstrap/self_reflection.py +155 -0
  231. apsimo/identity_bootstrap/skill.py +37 -0
  232. apsimo/identity_bootstrap/verifier.py +436 -0
  233. apsimo/initiatives/__init__.py +20 -0
  234. apsimo/initiatives/action_registry.py +454 -0
  235. apsimo/initiatives/approval_authority.py +2105 -0
  236. apsimo/initiatives/approval_policy.py +123 -0
  237. apsimo/initiatives/assignment.py +263 -0
  238. apsimo/initiatives/backup_evidence.py +100 -0
  239. apsimo/initiatives/context_freshness.py +103 -0
  240. apsimo/initiatives/models.py +318 -0
  241. apsimo/initiatives/native_work.py +270 -0
  242. apsimo/initiatives/standing_approvals.py +232 -0
  243. apsimo/initiatives/store.py +1081 -0
  244. apsimo/initiatives/temporal_followup.py +410 -0
  245. apsimo/intelligence/__init__.py +1 -0
  246. apsimo/intelligence/cognition/__init__.py +24 -0
  247. apsimo/intelligence/cognition/gap_detector.py +148 -0
  248. apsimo/intelligence/cognition/metalearner.py +547 -0
  249. apsimo/intelligence/cognition/metrics_collector.py +217 -0
  250. apsimo/intelligence/cognition/performance_index.py +299 -0
  251. apsimo/intelligence/cognition/registry.py +192 -0
  252. apsimo/intelligence/cognition/strategy_adjuster.py +222 -0
  253. apsimo/intelligence/cognition/types.py +16 -0
  254. apsimo/intelligence/components/__init__.py +66 -0
  255. apsimo/intelligence/components/anomaly_detector.py +413 -0
  256. apsimo/intelligence/components/initiative_engine.py +2643 -0
  257. apsimo/intelligence/components/preference_learner.py +521 -0
  258. apsimo/intelligence/components/research_orchestrator.py +358 -0
  259. apsimo/intelligence/components/self_directed_thinker.py +221 -0
  260. apsimo/intelligence/components/self_reflector.py +252 -0
  261. apsimo/intelligence/components/session_continuity.py +154 -0
  262. apsimo/intelligence/components/task_planner.py +320 -0
  263. apsimo/intelligence/components/tool_learner.py +217 -0
  264. apsimo/intelligence/graph/__init__.py +79 -0
  265. apsimo/intelligence/graph/client.py +2483 -0
  266. apsimo/intelligence/graph/consolidator.py +405 -0
  267. apsimo/intelligence/graph/distiller.py +312 -0
  268. apsimo/intelligence/graph/migrations.py +129 -0
  269. apsimo/intelligence/graph/queries.py +248 -0
  270. apsimo/intelligence/graph/recall.py +281 -0
  271. apsimo/intelligence/graph/reconciler.py +144 -0
  272. apsimo/intelligence/graph/schema.py +337 -0
  273. apsimo/intelligence/graph/selection.py +252 -0
  274. apsimo/intelligence/learning/__init__.py +17 -0
  275. apsimo/intelligence/learning/continuous_learner.py +245 -0
  276. apsimo/intelligence/learning/feedback_store.py +321 -0
  277. apsimo/intelligence/mind_model/__init__.py +1 -0
  278. apsimo/intelligence/mind_model/graph_baseline.py +136 -0
  279. apsimo/intelligence/mind_model/signal_collector.py +361 -0
  280. apsimo/intelligence/relationships/__init__.py +11 -0
  281. apsimo/intelligence/relationships/profiler.py +389 -0
  282. apsimo/intelligence/relationships/scorer.py +560 -0
  283. apsimo/intelligence/relationships/signal_floor.py +66 -0
  284. apsimo/intelligence/relationships/trust_tiers.py +300 -0
  285. apsimo/intelligence/synthesis/__init__.py +40 -0
  286. apsimo/intelligence/synthesis/connection_discoverer.py +379 -0
  287. apsimo/intelligence/synthesis/cross_domain_analyzer.py +287 -0
  288. apsimo/intelligence/synthesis/insight_deliverer.py +171 -0
  289. apsimo/intelligence/synthesis/insight_store.py +79 -0
  290. apsimo/intelligence/synthesis/insight_validator.py +183 -0
  291. apsimo/intelligence/synthesis/novelty_scorer.py +267 -0
  292. apsimo/intelligence/turn_middleware/__init__.py +15 -0
  293. apsimo/intelligence/turn_middleware/memory_sync.py +119 -0
  294. apsimo/mcp/__init__.py +41 -0
  295. apsimo/mcp/__main__.py +6 -0
  296. apsimo/mcp/config.py +287 -0
  297. apsimo/mcp/server.py +501 -0
  298. apsimo/migrations.py +187 -0
  299. apsimo/mining/__init__.py +27 -0
  300. apsimo/mining/corpus.py +239 -0
  301. apsimo/mining/escalations.py +289 -0
  302. apsimo/mining/models.py +169 -0
  303. apsimo/mining/store.py +210 -0
  304. apsimo/models/__init__.py +30 -0
  305. apsimo/models/memory.py +80 -0
  306. apsimo/models/mesh.py +72 -0
  307. apsimo/models/person.py +104 -0
  308. apsimo/models/signal.py +108 -0
  309. apsimo/observations/__init__.py +15 -0
  310. apsimo/observations/store.py +277 -0
  311. apsimo/patterns/__init__.py +6 -0
  312. apsimo/patterns/extract.py +187 -0
  313. apsimo/patterns/store.py +227 -0
  314. apsimo/persona/__init__.py +1 -0
  315. apsimo/persona/engine.py +611 -0
  316. apsimo/persona/manifest.py +140 -0
  317. apsimo/projects/__init__.py +28 -0
  318. apsimo/projects/engine.py +1681 -0
  319. apsimo/projects/event_outbox.py +188 -0
  320. apsimo/projects/models.py +216 -0
  321. apsimo/projects/planner.py +181 -0
  322. apsimo/projects/store.py +1446 -0
  323. apsimo/proposals/__init__.py +12 -0
  324. apsimo/proposals/engine.py +114 -0
  325. apsimo/proposals/models.py +207 -0
  326. apsimo/qualification/__init__.py +1 -0
  327. apsimo/qualification/cases.py +75 -0
  328. apsimo/qualification/cli.py +51 -0
  329. apsimo/qualification/memory_cases.py +209 -0
  330. apsimo/qualification/records.py +92 -0
  331. apsimo/qualification/report.py +87 -0
  332. apsimo/qualification/runner.py +311 -0
  333. apsimo/qualification/structured_cases.py +131 -0
  334. apsimo/reasoning/__init__.py +13 -0
  335. apsimo/reasoning/executor.py +506 -0
  336. apsimo/reasoning/loop.py +373 -0
  337. apsimo/reasoning/native_tools/__init__.py +16 -0
  338. apsimo/reasoning/native_tools/calculate.py +141 -0
  339. apsimo/reasoning/native_tools/file_ops.py +150 -0
  340. apsimo/reasoning/native_tools/web_search.py +49 -0
  341. apsimo/reasoning/tool_policy.py +182 -0
  342. apsimo/redact/__init__.py +176 -0
  343. apsimo/repos/__init__.py +5 -0
  344. apsimo/repos/mirrors.py +204 -0
  345. apsimo/research/__init__.py +41 -0
  346. apsimo/research/artifact.py +482 -0
  347. apsimo/research/gatherer.py +387 -0
  348. apsimo/research/pipeline.py +513 -0
  349. apsimo/research/search/__init__.py +7 -0
  350. apsimo/research/search/base.py +41 -0
  351. apsimo/research/search/brave.py +59 -0
  352. apsimo/research/search/cache.py +51 -0
  353. apsimo/research/search/duckduckgo.py +103 -0
  354. apsimo/research/search/orchestrator.py +119 -0
  355. apsimo/research/search/serpapi.py +59 -0
  356. apsimo/research/search/tavily.py +59 -0
  357. apsimo/research/synthesizer.py +309 -0
  358. apsimo/router/__init__.py +30 -0
  359. apsimo/router/complexity_scorer.py +148 -0
  360. apsimo/router/endpoints.py +153 -0
  361. apsimo/router/fallback.py +58 -0
  362. apsimo/router/functions.py +243 -0
  363. apsimo/router/native_policy.py +52 -0
  364. apsimo/router/router.py +762 -0
  365. apsimo/router/self_learning.py +174 -0
  366. apsimo/router/tiers.py +677 -0
  367. apsimo/sandbox/__init__.py +21 -0
  368. apsimo/sandbox/backend.py +195 -0
  369. apsimo/sandbox/manager.py +173 -0
  370. apsimo/scope_bounds.py +7 -0
  371. apsimo/secrets/__init__.py +6 -0
  372. apsimo/secrets/backends/__init__.py +8 -0
  373. apsimo/secrets/backends/base.py +42 -0
  374. apsimo/secrets/backends/env.py +110 -0
  375. apsimo/secrets/backends/keyring.py +72 -0
  376. apsimo/secrets/backends/onepassword.py +232 -0
  377. apsimo/secrets/cli.py +191 -0
  378. apsimo/secrets/manager.py +160 -0
  379. apsimo/secrets/migration.py +101 -0
  380. apsimo/secrets/types.py +98 -0
  381. apsimo/seed.py +41 -0
  382. apsimo/self_model/__init__.py +37 -0
  383. apsimo/self_model/appraisals.py +673 -0
  384. apsimo/self_model/benchmark.py +1314 -0
  385. apsimo/self_model/brief.py +40 -0
  386. apsimo/self_model/event_concerns.py +1128 -0
  387. apsimo/self_model/execution_forecasts.py +353 -0
  388. apsimo/self_model/expectations.py +1595 -0
  389. apsimo/self_model/experiments.py +1150 -0
  390. apsimo/self_model/journal.py +148 -0
  391. apsimo/self_model/judgments.py +705 -0
  392. apsimo/self_model/native_outcomes.py +55 -0
  393. apsimo/self_model/params.py +220 -0
  394. apsimo/self_model/perspective.py +246 -0
  395. apsimo/self_model/reconcile.py +183 -0
  396. apsimo/self_model/reply_forecasts.py +381 -0
  397. apsimo/self_model/runtime_forecasts.py +296 -0
  398. apsimo/self_model/runtime_models.py +67 -0
  399. apsimo/self_model/settlement.py +207 -0
  400. apsimo/self_model/situation.py +1731 -0
  401. apsimo/self_model/store.py +883 -0
  402. apsimo/self_model/supervised.py +137 -0
  403. apsimo/self_model/thinker.py +99 -0
  404. apsimo/self_model/trust.py +388 -0
  405. apsimo/self_model/workspace.py +2388 -0
  406. apsimo/server.py +4197 -0
  407. apsimo/services/__init__.py +1 -0
  408. apsimo/services/agent_bridge.py +474 -0
  409. apsimo/services/initiative_executor.py +914 -0
  410. apsimo/services/instance.py +297 -0
  411. apsimo/sessions/__init__.py +22 -0
  412. apsimo/sessions/config.py +13 -0
  413. apsimo/sessions/context_loader.py +88 -0
  414. apsimo/sessions/federation_session.py +75 -0
  415. apsimo/sessions/isolated_session.py +98 -0
  416. apsimo/sessions/reports.py +84 -0
  417. apsimo/sessions/store.py +148 -0
  418. apsimo/setup.py +2818 -0
  419. apsimo/setup_hermes.py +879 -0
  420. apsimo/setup_local_work.py +218 -0
  421. apsimo/setup_native_goals.py +134 -0
  422. apsimo/setup_native_reviews.py +115 -0
  423. apsimo/skills/__init__.py +10 -0
  424. apsimo/skills/base.py +108 -0
  425. apsimo/skills/budget.py +28 -0
  426. apsimo/skills/executor.py +493 -0
  427. apsimo/skills/executors/__init__.py +1 -0
  428. apsimo/skills/executors/behavioral_correction.py +75 -0
  429. apsimo/skills/executors/capability_gap.py +38 -0
  430. apsimo/skills/executors/data_quality.py +163 -0
  431. apsimo/skills/executors/knowledge_acquisition.py +41 -0
  432. apsimo/skills/executors/operational_hygiene.py +185 -0
  433. apsimo/skills/executors/subsystem_health.py +169 -0
  434. apsimo/skills/hermes_export.py +431 -0
  435. apsimo/skills/index.py +123 -0
  436. apsimo/skills/learning/__init__.py +21 -0
  437. apsimo/skills/learning/novelty_detector.py +206 -0
  438. apsimo/skills/learning/pattern_extractor.py +199 -0
  439. apsimo/skills/learning/triggers.py +159 -0
  440. apsimo/skills/loader.py +246 -0
  441. apsimo/skills/migrations/002_progressive_loading.sql +6 -0
  442. apsimo/skills/migrations/backfill_triggers.py +20 -0
  443. apsimo/skills/models.py +202 -0
  444. apsimo/skills/packager.py +128 -0
  445. apsimo/skills/protocols.py +70 -0
  446. apsimo/skills/registry.py +191 -0
  447. apsimo/skills/runtime.py +58 -0
  448. apsimo/skills/sandbox_runner.py +229 -0
  449. apsimo/skills/scheduler.py +129 -0
  450. apsimo/skills/schema.py +79 -0
  451. apsimo/skills/security/__init__.py +12 -0
  452. apsimo/skills/security/guards.py +53 -0
  453. apsimo/skills/security/scanner.py +223 -0
  454. apsimo/skills_memory/__init__.py +26 -0
  455. apsimo/skills_memory/distill.py +159 -0
  456. apsimo/skills_memory/models.py +85 -0
  457. apsimo/skills_memory/retrieve.py +62 -0
  458. apsimo/skills_memory/store.py +172 -0
  459. apsimo/surprise/__init__.py +6 -0
  460. apsimo/surprise/accumulation.py +57 -0
  461. apsimo/surprise/scorer.py +102 -0
  462. apsimo/surprise/store.py +203 -0
  463. apsimo/task_queue/__init__.py +69 -0
  464. apsimo/task_queue/action_receipts.py +148 -0
  465. apsimo/task_queue/approval_relay_canary.py +108 -0
  466. apsimo/task_queue/config.py +85 -0
  467. apsimo/task_queue/contract.py +361 -0
  468. apsimo/task_queue/events.py +130 -0
  469. apsimo/task_queue/governor.py +1031 -0
  470. apsimo/task_queue/handlers/__init__.py +16 -0
  471. apsimo/task_queue/handlers/base.py +37 -0
  472. apsimo/task_queue/handlers/inference.py +640 -0
  473. apsimo/task_queue/handlers/monitoring.py +116 -0
  474. apsimo/task_queue/handlers/registry.py +75 -0
  475. apsimo/task_queue/handlers/subtask_handler.py +173 -0
  476. apsimo/task_queue/handlers/system_maintenance.py +147 -0
  477. apsimo/task_queue/mesh_integration.py +111 -0
  478. apsimo/task_queue/models.py +317 -0
  479. apsimo/task_queue/queue_manager.py +8286 -0
  480. apsimo/task_queue/routing.py +287 -0
  481. apsimo/task_queue/scheduler.py +252 -0
  482. apsimo/task_queue/schema.sql +197 -0
  483. apsimo/task_queue/work_control.py +342 -0
  484. apsimo/task_queue/worker.py +993 -0
  485. apsimo/telemetry.py +145 -0
  486. apsimo/tom/__init__.py +6 -0
  487. apsimo/tom/affect.py +387 -0
  488. apsimo/tom/approvals.py +171 -0
  489. apsimo/tom/arcs.py +896 -0
  490. apsimo/tom/asymmetry.py +131 -0
  491. apsimo/tom/eligibility.py +248 -0
  492. apsimo/tom/engagement.py +214 -0
  493. apsimo/tom/exposure.py +214 -0
  494. apsimo/tom/extractor.py +306 -0
  495. apsimo/tom/fact_adapters.py +144 -0
  496. apsimo/tom/facts.py +326 -0
  497. apsimo/tom/integration.py +592 -0
  498. apsimo/tom/leveled.py +118 -0
  499. apsimo/tom/levels.py +247 -0
  500. apsimo/tom/recipient_audit.py +995 -0
  501. apsimo/tom/recipient_simulator.py +593 -0
  502. apsimo/tom/source_lineage.py +93 -0
  503. apsimo/tom/tom2.py +277 -0
  504. apsimo/tom/visibility.py +559 -0
  505. apsimo/tom/visibility_store.py +414 -0
  506. apsimo/tools/__init__.py +0 -0
  507. apsimo/tools/definitions.py +740 -0
  508. apsimo/tools/handlers.py +943 -0
  509. apsimo/toolsmith/__init__.py +26 -0
  510. apsimo/toolsmith/authority.py +166 -0
  511. apsimo/toolsmith/engine.py +559 -0
  512. apsimo/toolsmith/integrity.py +100 -0
  513. apsimo/toolsmith/miner.py +145 -0
  514. apsimo/toolsmith/policy.py +110 -0
  515. apsimo/toolsmith/registry.py +635 -0
  516. apsimo/turns/__init__.py +17 -0
  517. apsimo/turns/audio.py +134 -0
  518. apsimo/turns/documents.py +235 -0
  519. apsimo/turns/executions.py +486 -0
  520. apsimo/turns/hermes_history.py +245 -0
  521. apsimo/turns/hermes_kanban.py +268 -0
  522. apsimo/turns/hermes_work.py +96 -0
  523. apsimo/turns/idempotency.py +752 -0
  524. apsimo/turns/local_work.py +115 -0
  525. apsimo/turns/media.py +581 -0
  526. apsimo/turns/reported_workers.py +196 -0
  527. apsimo/turns/source_annotations.py +283 -0
  528. apsimo/turns/source_attribution.py +154 -0
  529. apsimo/turns/source_read.py +351 -0
  530. apsimo/turns/source_vectors.py +263 -0
  531. apsimo/turns/video.py +210 -0
  532. apsimo/util/autonomy_preset.py +220 -0
  533. apsimo/util/instance.py +92 -0
  534. apsimo/util/model_output.py +25 -0
  535. apsimo/util/quiet_hours.py +27 -0
  536. apsimo/util/session_safety.py +37 -0
  537. apsimo/util/temporal.py +343 -0
  538. apsimo/vector/__init__.py +75 -0
  539. apsimo/vector/backfill.py +171 -0
  540. apsimo/vector/caption.py +114 -0
  541. apsimo/vector/collections.py +51 -0
  542. apsimo/vector/config.py +102 -0
  543. apsimo/vector/embedder.py +670 -0
  544. apsimo/vector/image_preprocess.py +406 -0
  545. apsimo/vector/image_store.py +296 -0
  546. apsimo/vector/indexes.py +162 -0
  547. apsimo/vector/migrate.py +334 -0
  548. apsimo/vector/multimodal_provider.py +417 -0
  549. apsimo/vector/multimodal_types.py +87 -0
  550. apsimo/vector/openai_provider.py +119 -0
  551. apsimo/vector/query.py +49 -0
  552. apsimo/vector/reranker.py +565 -0
  553. apsimo/vector/safety_image.py +159 -0
  554. apsimo/vector/scanner.py +197 -0
  555. apsimo/vector/setup.py +289 -0
  556. apsimo/vector/store.py +533 -0
  557. apsimo/vector/tiers.py +263 -0
  558. apsimo/work_orders.py +925 -0
  559. apsimo/workers/__init__.py +21 -0
  560. apsimo/workers/agent_bridge.py +640 -0
  561. apsimo/workers/colony_worker.py +382 -0
  562. apsimo/workers/queue_worker.py +441 -0
  563. apsimo/workers/skills_sync.py +152 -0
  564. apsimo/world_model/__init__.py +71 -0
  565. apsimo/world_model/causal_maintenance.py +131 -0
  566. apsimo/world_model/causal_policy.py +43 -0
  567. apsimo/world_model/causal_query.py +125 -0
  568. apsimo/world_model/confidence.py +54 -0
  569. apsimo/world_model/config.py +64 -0
  570. apsimo/world_model/constants.py +97 -0
  571. apsimo/world_model/entities.py +145 -0
  572. apsimo/world_model/expectation_resolvers.py +177 -0
  573. apsimo/world_model/extraction/__init__.py +7 -0
  574. apsimo/world_model/extraction/base.py +62 -0
  575. apsimo/world_model/extraction/conversation_extractor.py +262 -0
  576. apsimo/world_model/extraction/detector.py +74 -0
  577. apsimo/world_model/extraction/document_extractor.py +78 -0
  578. apsimo/world_model/extraction/formats/__init__.py +24 -0
  579. apsimo/world_model/extraction/formats/csv_fmt.py +68 -0
  580. apsimo/world_model/extraction/formats/html_fmt.py +72 -0
  581. apsimo/world_model/extraction/formats/json_fmt.py +68 -0
  582. apsimo/world_model/extraction/formats/pdf.py +43 -0
  583. apsimo/world_model/extraction/formats/text.py +27 -0
  584. apsimo/world_model/extraction/llm_extractor.py +164 -0
  585. apsimo/world_model/extraction/pipeline.py +73 -0
  586. apsimo/world_model/integrations/__init__.py +5 -0
  587. apsimo/world_model/integrations/mind_model_bridge.py +115 -0
  588. apsimo/world_model/integrations/social_intel_bridge.py +120 -0
  589. apsimo/world_model/jobs/__init__.py +4 -0
  590. apsimo/world_model/jobs/extraction_job.py +168 -0
  591. apsimo/world_model/llm_extract.py +572 -0
  592. apsimo/world_model/neo4j/__init__.py +5 -0
  593. apsimo/world_model/neo4j/backend.py +654 -0
  594. apsimo/world_model/observations.py +155 -0
  595. apsimo/world_model/populator.py +307 -0
  596. apsimo/world_model/postgres/__init__.py +1 -0
  597. apsimo/world_model/postgres/backend.py +683 -0
  598. apsimo/world_model/relationships.py +25 -0
  599. apsimo/world_model/resolution/__init__.py +13 -0
  600. apsimo/world_model/resolution/entity_resolver.py +232 -0
  601. apsimo/world_model/resolution/merge_audit.py +16 -0
  602. apsimo/world_model/resolution/merge_workflow.py +117 -0
  603. apsimo/world_model/source_reports.py +121 -0
  604. apsimo/world_model/sqlite/__init__.py +4 -0
  605. apsimo/world_model/sqlite/backend.py +855 -0
  606. apsimo/world_model/sqlite/schema.sql +132 -0
  607. apsimo/world_model/store.py +545 -0
  608. apsimo-1.3.0.dist-info/METADATA +78 -0
  609. apsimo-1.3.0.dist-info/RECORD +614 -0
  610. apsimo-1.3.0.dist-info/WHEEL +5 -0
  611. apsimo-1.3.0.dist-info/entry_points.txt +11 -0
  612. apsimo-1.3.0.dist-info/licenses/LICENSE +21 -0
  613. apsimo-1.3.0.dist-info/top_level.txt +2 -0
  614. colony_sidecar/__init__.py +4 -0
@@ -0,0 +1,232 @@
1
+ """1Password secrets backend — supports op CLI and 1Password Connect Server API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import shutil
8
+ import subprocess
9
+ from dataclasses import dataclass, field
10
+
11
+ from apsimo.secrets.backends.base import SecretsBackend
12
+ from apsimo.secrets.types import SecretType, VAULT_NAME
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ @dataclass
18
+ class OPItem:
19
+ """Represents a 1Password item as used by Colony."""
20
+
21
+ item_id: str
22
+ title: str # Secret key (e.g., "OPENAI_API_KEY")
23
+ value: str
24
+ category: str # Derived from SecretType
25
+ tags: list[str] = field(default_factory=list)
26
+
27
+
28
+ class OnePasswordBackend(SecretsBackend):
29
+ """1Password secrets backend.
30
+
31
+ Supports two modes:
32
+ 1. CLI mode (default): uses ``op`` CLI — requires 1Password desktop app
33
+ or 1Password CLI with biometric unlock configured.
34
+ 2. Connect mode: uses 1Password Connect Server API — suitable for
35
+ headless/server deployments. Requires OP_CONNECT_HOST and OP_CONNECT_TOKEN.
36
+
37
+ Vault structure:
38
+ - Default vault name: "Colony" (overridable in config)
39
+ - Items organised by section: LLM Keys, Infrastructure, Messaging
40
+ Gateways, Mesh / Federation, Email, Backup, Push Notifications,
41
+ Colony Core.
42
+
43
+ Auto-detects mode based on available configuration.
44
+ """
45
+
46
+ name = "1password"
47
+
48
+ # 1Password item sections by SecretType
49
+ SECTION_MAP: dict[SecretType, str] = {
50
+ SecretType.LLM_API_KEY: "LLM Keys",
51
+ SecretType.NEO4J_CREDENTIAL: "Infrastructure",
52
+ SecretType.GATEWAY_TOKEN: "Messaging Gateways",
53
+ SecretType.MESH_PAIRING_KEY: "Mesh / Federation",
54
+ SecretType.EMAIL_CREDENTIAL: "Email",
55
+ SecretType.BACKUP_PASSPHRASE: "Backup",
56
+ SecretType.COLONY_TOKEN: "Colony Core",
57
+ SecretType.FEDERATION_CERT: "Mesh / Federation",
58
+ SecretType.PUSH_CREDENTIAL: "Push Notifications",
59
+ SecretType.CALDAV_CREDENTIAL: "Calendar",
60
+ SecretType.HEALTH_CREDENTIAL: "Health",
61
+ SecretType.OTHER: "Miscellaneous",
62
+ }
63
+
64
+ def __init__(
65
+ self,
66
+ vault_name: str = VAULT_NAME,
67
+ connect_host: str | None = None,
68
+ connect_token: str | None = None,
69
+ ) -> None:
70
+ self._vault = vault_name
71
+ self._connect_host = connect_host
72
+ self._connect_token = connect_token
73
+ self._use_connect = bool(connect_host and connect_token)
74
+ self._cache: dict[str, str] = {} # in-process cache
75
+
76
+ def is_available(self) -> bool:
77
+ if self._use_connect:
78
+ return bool(self._connect_host and self._connect_token)
79
+ return shutil.which("op") is not None
80
+
81
+ def get(self, key: str, default: str | None = None) -> str | None:
82
+ if key in self._cache:
83
+ return self._cache[key]
84
+ if self._use_connect:
85
+ return self._get_via_connect(key, default)
86
+ return self._get_via_cli(key, default)
87
+
88
+ def set(self, key: str, value: str, *, secret_type: SecretType | None = None) -> None:
89
+ if self._use_connect:
90
+ self._set_via_connect(key, value, secret_type)
91
+ else:
92
+ self._set_via_cli(key, value, secret_type)
93
+ self._cache[key] = value
94
+
95
+ def delete(self, key: str) -> None:
96
+ try:
97
+ self._run_op(["item", "delete", self._item_title(key), f"--vault={self._vault}"])
98
+ except subprocess.CalledProcessError as exc:
99
+ logger.debug("1Password delete failed for %s: %s", key, exc)
100
+ self._cache.pop(key, None)
101
+
102
+ def list(self) -> list[str]:
103
+ try:
104
+ result = self._run_op([
105
+ "item", "list",
106
+ f"--vault={self._vault}",
107
+ "--format=json",
108
+ "--tags=colony",
109
+ ])
110
+ items = json.loads(result)
111
+ prefix = "colony:"
112
+ return [
113
+ item["title"][len(prefix):]
114
+ if item["title"].startswith(prefix)
115
+ else item["title"]
116
+ for item in items
117
+ ]
118
+ except (subprocess.CalledProcessError, json.JSONDecodeError) as exc:
119
+ logger.debug("1Password list failed: %s", exc)
120
+ return []
121
+
122
+ # -------------------------------------------------------------------------
123
+ # CLI helpers
124
+ # -------------------------------------------------------------------------
125
+
126
+ def _get_via_cli(self, key: str, default: str | None) -> str | None:
127
+ try:
128
+ result = self._run_op([
129
+ "item", "get", self._item_title(key),
130
+ f"--vault={self._vault}",
131
+ "--fields=password",
132
+ "--format=json",
133
+ ])
134
+ data = json.loads(result)
135
+ # op returns {"value": "..."} or {"password": "..."} depending on version
136
+ value = data.get("value") or data.get("password")
137
+ if value:
138
+ self._cache[key] = value
139
+ return value or default
140
+ except (subprocess.CalledProcessError, json.JSONDecodeError):
141
+ return default
142
+
143
+ def _set_via_cli(
144
+ self, key: str, value: str, secret_type: SecretType | None
145
+ ) -> None:
146
+ section = self.SECTION_MAP.get(secret_type or SecretType.OTHER, "Miscellaneous")
147
+ title = self._item_title(key)
148
+
149
+ # Try update first, create if item not found
150
+ try:
151
+ self._run_op([
152
+ "item", "edit", title,
153
+ f"--vault={self._vault}",
154
+ f"password={value}",
155
+ ])
156
+ except subprocess.CalledProcessError:
157
+ self._run_op([
158
+ "item", "create",
159
+ "--category=login",
160
+ f"--title={title}",
161
+ f"--vault={self._vault}",
162
+ f"--section={section}",
163
+ f"password={value}",
164
+ "--tags=colony",
165
+ ])
166
+
167
+ def _run_op(self, args: list[str]) -> str:
168
+ result = subprocess.run(
169
+ ["op"] + args,
170
+ capture_output=True,
171
+ text=True,
172
+ check=True,
173
+ timeout=10,
174
+ )
175
+ return result.stdout.strip()
176
+
177
+ @staticmethod
178
+ def _item_title(key: str) -> str:
179
+ """Map a secret key to a 1Password item title."""
180
+ return f"colony:{key}"
181
+
182
+ # -------------------------------------------------------------------------
183
+ # 1Password Connect Server API helpers
184
+ # -------------------------------------------------------------------------
185
+
186
+ def _get_via_connect(self, key: str, default: str | None) -> str | None:
187
+ """Retrieve via 1Password Connect Server REST API."""
188
+ import urllib.request
189
+ url = f"{self._connect_host}/v1/vaults/{self._vault}/items"
190
+ req = urllib.request.Request(
191
+ url,
192
+ headers={"Authorization": f"Bearer {self._connect_token}"},
193
+ )
194
+ try:
195
+ with urllib.request.urlopen(req, timeout=5) as resp:
196
+ items = json.loads(resp.read())
197
+ target = self._item_title(key)
198
+ for item in items:
199
+ if item.get("title") == target:
200
+ fields = item.get("fields", [])
201
+ if fields:
202
+ value = fields[0].get("value")
203
+ if value:
204
+ self._cache[key] = value
205
+ return value
206
+ except Exception as exc:
207
+ logger.debug("1Password Connect get failed for %s: %s", key, exc)
208
+ return default
209
+
210
+ def _set_via_connect(
211
+ self, key: str, value: str, secret_type: SecretType | None
212
+ ) -> None:
213
+ """Store via 1Password Connect Server REST API."""
214
+ import urllib.request
215
+ section = self.SECTION_MAP.get(secret_type or SecretType.OTHER, "Miscellaneous")
216
+ payload = json.dumps({
217
+ "title": self._item_title(key),
218
+ "category": "LOGIN",
219
+ "tags": ["colony"],
220
+ "sections": [{"label": section}],
221
+ "fields": [{"label": "password", "value": value, "type": "CONCEALED"}],
222
+ }).encode()
223
+ req = urllib.request.Request(
224
+ f"{self._connect_host}/v1/vaults/{self._vault}/items",
225
+ data=payload,
226
+ headers={
227
+ "Authorization": f"Bearer {self._connect_token}",
228
+ "Content-Type": "application/json",
229
+ },
230
+ method="POST",
231
+ )
232
+ urllib.request.urlopen(req, timeout=5)
apsimo/secrets/cli.py ADDED
@@ -0,0 +1,191 @@
1
+ """CLI command handlers for `colony secrets`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ import argparse
10
+
11
+
12
+ # -------------------------------------------------------------------------
13
+ # Section grouping for pretty-print
14
+ # -------------------------------------------------------------------------
15
+
16
+ _SECTION_KEYS: dict[str, list[str]] = {
17
+ "LLM Keys": [
18
+ "ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY",
19
+ "GEMINI_API_KEY", "GROQ_API_KEY", "TOGETHER_API_KEY",
20
+ ],
21
+ "Infrastructure": ["NEO4J_URI", "NEO4J_USERNAME", "NEO4J_PASSWORD", "COLONY_TOKEN_SECRET"],
22
+ "Messaging Gateways": [
23
+ "TELEGRAM_BOT_TOKEN", "DISCORD_BOT_TOKEN", "SLACK_BOT_TOKEN",
24
+ "WHATSAPP_API_KEY", "TWILIO_ACCOUNT_SID", "TWILIO_AUTH_TOKEN", "SIGNAL_API_KEY",
25
+ ],
26
+ "Email": ["EMAIL_IMAP_PASSWORD", "EMAIL_SMTP_PASSWORD"],
27
+ "Backup": ["BACKUP_PASSPHRASE", "S3_ACCESS_KEY", "S3_SECRET_KEY"],
28
+ "Push Notifications": ["APNS_AUTH_KEY", "APNS_KEY_ID", "APNS_TEAM_ID", "FCM_SERVICE_ACCOUNT_JSON"],
29
+ "Calendar / Health": ["CALDAV_PASSWORD", "OURA_PERSONAL_ACCESS_TOKEN"],
30
+ "1Password Connect": ["OP_CONNECT_HOST", "OP_CONNECT_TOKEN"],
31
+ }
32
+
33
+
34
+ def _get_manager():
35
+ from apsimo.secrets.manager import SecretsManager
36
+ return SecretsManager()
37
+
38
+
39
+ def cmd_secrets_list(args: "argparse.Namespace") -> None:
40
+ """colony secrets list — show configured secrets grouped by category."""
41
+ sm = _get_manager()
42
+ configured = set(sm.list())
43
+
44
+ print(f"\nBackend: {sm.backend_name}\n")
45
+
46
+ unconfigured: list[str] = []
47
+ for section, keys in _SECTION_KEYS.items():
48
+ section_configured = [k for k in keys if k in configured]
49
+ section_missing = [k for k in keys if k not in configured]
50
+ unconfigured.extend(section_missing)
51
+ if section_configured:
52
+ print(f"{section} ({len(section_configured)} configured):")
53
+ for k in section_configured:
54
+ print(f" \u2713 {k}")
55
+ print()
56
+
57
+ if unconfigured:
58
+ shown = unconfigured[:10]
59
+ remaining = len(unconfigured) - len(shown)
60
+ print("Not configured (run `colony secrets set KEY VALUE`):")
61
+ for k in shown:
62
+ print(f" \u2717 {k}")
63
+ if remaining > 0:
64
+ print(f" ... ({remaining} more)")
65
+ print()
66
+
67
+
68
+ def cmd_secrets_get(args: "argparse.Namespace") -> None:
69
+ """colony secrets get KEY — retrieve a secret."""
70
+ sm = _get_manager()
71
+ value = sm.get(args.key)
72
+ if value is None:
73
+ print(f"Secret '{args.key}' not found in {sm.backend_name} backend.", file=sys.stderr)
74
+ sys.exit(1)
75
+ print(value)
76
+
77
+
78
+ def cmd_secrets_set(args: "argparse.Namespace") -> None:
79
+ """colony secrets set KEY VALUE — store a secret."""
80
+ from apsimo.secrets.types import infer_secret_type
81
+ sm = _get_manager()
82
+ secret_type = infer_secret_type(args.key)
83
+ sm.set(args.key, args.value, secret_type=secret_type)
84
+ print(f"\u2713 Set {args.key} in {sm.backend_name} backend.")
85
+
86
+
87
+ def cmd_secrets_delete(args: "argparse.Namespace") -> None:
88
+ """colony secrets delete KEY — remove a secret."""
89
+ sm = _get_manager()
90
+ sm.delete(args.key)
91
+ print(f"\u2713 Deleted {args.key} from {sm.backend_name} backend.")
92
+
93
+
94
+ def cmd_secrets_backend(args: "argparse.Namespace") -> None:
95
+ """colony secrets backend — show current backend."""
96
+ sm = _get_manager()
97
+ print(f"Current secrets backend: {sm.backend_name}")
98
+ if sm.backend_name == "1password":
99
+ print(" Storage: 1Password vault")
100
+ elif sm.backend_name == "keyring":
101
+ print(" Storage: System keychain (macOS Keychain / Linux SecretService)")
102
+ else:
103
+ print(" Storage: ~/.colony/.env (plaintext, mode 0600)")
104
+ print("\nChange backend: colony setup (re-run onboarding wizard)")
105
+
106
+
107
+ def cmd_secrets_status(args: "argparse.Namespace") -> None:
108
+ """colony secrets status — check backend availability."""
109
+ from apsimo.secrets.backends.env import EnvBackend
110
+ from apsimo.secrets.backends.keyring import KeyringBackend
111
+ from apsimo.secrets.backends.onepassword import OnePasswordBackend
112
+
113
+ sm = _get_manager()
114
+ print(f"\nActive backend: {sm.backend_name}")
115
+ print()
116
+
117
+ backends = [
118
+ ("1password", OnePasswordBackend()),
119
+ ("keyring", KeyringBackend()),
120
+ ("env", EnvBackend()),
121
+ ]
122
+ for name, backend in backends:
123
+ mark = "\u2713" if backend.is_available() else "\u2717"
124
+ active = " (active)" if name == sm.backend_name else ""
125
+ print(f" {mark} {name}{active}")
126
+
127
+ print()
128
+ configured = sm.list()
129
+ print(f"Configured secrets: {len(configured)}")
130
+
131
+
132
+ def cmd_secrets_migrate(args: "argparse.Namespace") -> None:
133
+ """colony secrets migrate — move secrets between backends."""
134
+ from apsimo.secrets.backends.env import EnvBackend
135
+ from apsimo.secrets.backends.keyring import KeyringBackend
136
+ from apsimo.secrets.backends.onepassword import OnePasswordBackend
137
+ from apsimo.secrets.migration import SecretsMigrator
138
+
139
+ _backend_map = {
140
+ "env": EnvBackend,
141
+ "keyring": KeyringBackend,
142
+ "1password": OnePasswordBackend,
143
+ }
144
+
145
+ from_name = args.from_backend
146
+ to_name = args.to_backend
147
+
148
+ if from_name not in _backend_map:
149
+ print(f"Unknown source backend: {from_name}", file=sys.stderr)
150
+ sys.exit(1)
151
+ if to_name not in _backend_map:
152
+ print(f"Unknown target backend: {to_name}", file=sys.stderr)
153
+ sys.exit(1)
154
+
155
+ source = _backend_map[from_name]()
156
+ target = _backend_map[to_name]()
157
+
158
+ if not target.is_available():
159
+ print(
160
+ f"Target backend '{to_name}' is not available on this system.",
161
+ file=sys.stderr,
162
+ )
163
+ sys.exit(1)
164
+
165
+ migrator = SecretsMigrator(source, target)
166
+
167
+ dry_run: bool = getattr(args, "dry_run", False)
168
+ delete_source: bool = getattr(args, "delete_source", False)
169
+
170
+ if dry_run:
171
+ keys = migrator.preview()
172
+ if not keys:
173
+ print(f"No secrets found in '{from_name}' backend.")
174
+ return
175
+ print(f"Would migrate {len(keys)} secrets from '{from_name}' to '{to_name}':")
176
+ print(" " + ", ".join(keys))
177
+ print("\nRun without --dry-run to proceed.")
178
+ return
179
+
180
+ print(f"Migrating secrets from '{from_name}' to '{to_name}'...")
181
+ result = migrator.migrate(dry_run=False, delete_from_source=delete_source)
182
+
183
+ for key in result.migrated:
184
+ print(f" \u2713 {key}")
185
+ for key, err in result.failed:
186
+ print(f" \u2717 {key}: {err}", file=sys.stderr)
187
+
188
+ print(f"\n{result.summary()}")
189
+
190
+ if delete_source and result.migrated:
191
+ print(f"Removed {len(result.migrated)} secrets from '{from_name}' backend.")
@@ -0,0 +1,160 @@
1
+ """SecretsManager — unified secrets access layer for Colony.
2
+
3
+ All Colony code MUST use SecretsManager to access secrets.
4
+ Direct reads from os.environ or .env files are DEPRECATED in V2
5
+ and will be removed in V3.
6
+
7
+ Usage::
8
+
9
+ sm = SecretsManager()
10
+ api_key = sm.get("OPENAI_API_KEY")
11
+ sm.set("OPENAI_API_KEY", "sk-...")
12
+ keys = sm.list()
13
+
14
+ The backend is loaded from ~/.colony/config.yaml on first instantiation
15
+ and cached for the process lifetime. Override ``COLONY_SECRETS_BACKEND``
16
+ env var to ``env``, ``keyring``, or ``1password`` for testing.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import logging
22
+ import os
23
+ from apsimo.secrets.backends.base import SecretsBackend
24
+ from apsimo.secrets.types import SecretType
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ _BACKEND_REGISTRY: dict[str, type[SecretsBackend]] = {}
29
+
30
+
31
+ def _register_backends() -> None:
32
+ """Lazy-load and register all known backends."""
33
+ global _BACKEND_REGISTRY
34
+ if _BACKEND_REGISTRY:
35
+ return
36
+ from apsimo.secrets.backends.env import EnvBackend
37
+ from apsimo.secrets.backends.keyring import KeyringBackend
38
+ from apsimo.secrets.backends.onepassword import OnePasswordBackend
39
+
40
+ _BACKEND_REGISTRY = {
41
+ "env": EnvBackend,
42
+ "keyring": KeyringBackend,
43
+ "1password": OnePasswordBackend,
44
+ }
45
+
46
+
47
+ class SecretsManager:
48
+ """Unified secrets access layer for Colony.
49
+
50
+ Singleton — one instance per process, backend determined from config.
51
+ Use ``SecretsManager.reset()`` in tests to clear cached state.
52
+ """
53
+
54
+ _instance: "SecretsManager | None" = None
55
+
56
+ def __new__(cls) -> "SecretsManager":
57
+ if cls._instance is None:
58
+ cls._instance = super().__new__(cls)
59
+ cls._instance._backend = cls._load_backend()
60
+ return cls._instance
61
+
62
+ @classmethod
63
+ def _load_backend(cls) -> SecretsBackend:
64
+ _register_backends()
65
+
66
+ # Allow override via env var (useful for testing and CI)
67
+ backend_name = os.environ.get("COLONY_SECRETS_BACKEND")
68
+
69
+ if not backend_name:
70
+ backend_name = cls._read_config_backend()
71
+
72
+ if not backend_name:
73
+ backend_name = "env"
74
+
75
+ if backend_name not in _BACKEND_REGISTRY:
76
+ logger.warning(
77
+ "Unknown secrets backend %r — falling back to 'env'", backend_name
78
+ )
79
+ backend_name = "env"
80
+
81
+ backend_cls = _BACKEND_REGISTRY[backend_name]
82
+ backend = backend_cls()
83
+
84
+ if not backend.is_available():
85
+ logger.warning(
86
+ "Secrets backend %r is not available on this system — "
87
+ "falling back to 'env'. Run `colony secrets status` for details.",
88
+ backend_name,
89
+ )
90
+ backend = _BACKEND_REGISTRY["env"]()
91
+
92
+ return backend
93
+
94
+ @classmethod
95
+ def _read_config_backend(cls) -> str | None:
96
+ """Read secrets.backend from ~/.colony/config.yaml."""
97
+ try:
98
+ import yaml
99
+ from pathlib import Path
100
+
101
+ colony_home = Path(os.environ.get("COLONY_HOME", Path.home() / ".colony"))
102
+ config_path = colony_home / "config.yaml"
103
+ if not config_path.exists():
104
+ return None
105
+ with open(config_path, encoding="utf-8") as f:
106
+ cfg = yaml.safe_load(f) or {}
107
+ return cfg.get("secrets", {}).get("backend")
108
+ except Exception as exc:
109
+ logger.debug("Could not read secrets backend from config: %s", exc)
110
+ return None
111
+
112
+ # -------------------------------------------------------------------------
113
+ # Public API
114
+ # -------------------------------------------------------------------------
115
+
116
+ def get(self, key: str, default: str | None = None) -> str | None:
117
+ """Retrieve a secret by key. Returns default if not found."""
118
+ return self._backend.get(key, default)
119
+
120
+ def get_required(self, key: str) -> str:
121
+ """Retrieve a secret by key. Raises KeyError if not found."""
122
+ value = self._backend.get(key)
123
+ if value is None:
124
+ raise KeyError(
125
+ f"Required secret '{key}' not found in {self._backend.name} backend. "
126
+ f"Run `colony secrets set {key} <value>` to configure it."
127
+ )
128
+ return value
129
+
130
+ def set(self, key: str, value: str, *, secret_type: SecretType | None = None) -> None:
131
+ """Store a secret."""
132
+ self._backend.set(key, value, secret_type=secret_type)
133
+
134
+ def delete(self, key: str) -> None:
135
+ """Remove a secret."""
136
+ self._backend.delete(key)
137
+
138
+ def list(self) -> list[str]:
139
+ """List all known secret keys (not values)."""
140
+ return self._backend.list()
141
+
142
+ @property
143
+ def backend_name(self) -> str:
144
+ return self._backend.name
145
+
146
+ @property
147
+ def backend(self) -> SecretsBackend:
148
+ return self._backend
149
+
150
+ @classmethod
151
+ def reset(cls) -> None:
152
+ """Clear the singleton — for testing only."""
153
+ cls._instance = None
154
+
155
+ @classmethod
156
+ def with_backend(cls, backend: SecretsBackend) -> "SecretsManager":
157
+ """Create a SecretsManager instance with a specific backend — for testing."""
158
+ instance = object.__new__(cls)
159
+ instance._backend = backend
160
+ return instance
@@ -0,0 +1,101 @@
1
+ """Auto-migration utilities for moving secrets between backends."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from dataclasses import dataclass, field
7
+
8
+ from apsimo.secrets.backends.base import SecretsBackend
9
+ from apsimo.secrets.types import infer_secret_type
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ @dataclass
15
+ class MigrationResult:
16
+ """Result summary from a secrets migration run."""
17
+
18
+ migrated: list[str] = field(default_factory=list)
19
+ skipped: list[str] = field(default_factory=list)
20
+ failed: list[tuple[str, str]] = field(default_factory=list)
21
+ would_migrate: list[str] = field(default_factory=list)
22
+
23
+ @property
24
+ def success_count(self) -> int:
25
+ return len(self.migrated)
26
+
27
+ @property
28
+ def failed_count(self) -> int:
29
+ return len(self.failed)
30
+
31
+ def summary(self) -> str:
32
+ parts = []
33
+ if self.would_migrate:
34
+ parts.append(f"Would migrate {len(self.would_migrate)} secrets (dry run)")
35
+ if self.migrated:
36
+ parts.append(f"Migrated {len(self.migrated)} secrets")
37
+ if self.skipped:
38
+ parts.append(f"Skipped {len(self.skipped)} (no value)")
39
+ if self.failed:
40
+ parts.append(f"Failed {len(self.failed)}")
41
+ return ", ".join(parts) if parts else "Nothing to migrate"
42
+
43
+
44
+ class SecretsMigrator:
45
+ """Detect existing secrets and migrate them between backends.
46
+
47
+ Called by the onboarding wizard when a user changes their secrets backend,
48
+ and optionally on first startup after a V2 upgrade if the user has a .env
49
+ file but has selected 1Password or keyring as their backend.
50
+ """
51
+
52
+ def __init__(self, source: SecretsBackend, target: SecretsBackend) -> None:
53
+ self._source = source
54
+ self._target = target
55
+
56
+ def preview(self) -> list[str]:
57
+ """Return list of secret keys that would be migrated."""
58
+ return self._source.list()
59
+
60
+ def migrate(
61
+ self,
62
+ keys: list[str] | None = None,
63
+ *,
64
+ dry_run: bool = False,
65
+ delete_from_source: bool = False,
66
+ ) -> MigrationResult:
67
+ """Migrate secrets from source to target backend.
68
+
69
+ Args:
70
+ keys: Specific keys to migrate. None = migrate all from source.
71
+ dry_run: If True, only report what would be migrated without writing.
72
+ delete_from_source: If True, delete from source after verifying target.
73
+
74
+ Returns:
75
+ MigrationResult with counts of migrated, skipped, and failed keys.
76
+ """
77
+ to_migrate = keys if keys is not None else self._source.list()
78
+ result = MigrationResult()
79
+
80
+ for key in to_migrate:
81
+ value = self._source.get(key)
82
+ if value is None:
83
+ result.skipped.append(key)
84
+ continue
85
+ if dry_run:
86
+ result.would_migrate.append(key)
87
+ continue
88
+ try:
89
+ secret_type = infer_secret_type(key)
90
+ self._target.set(key, value, secret_type=secret_type)
91
+ # Verify the write succeeded
92
+ if self._target.get(key) != value:
93
+ raise RuntimeError("Read-back verification failed")
94
+ result.migrated.append(key)
95
+ if delete_from_source:
96
+ self._source.delete(key)
97
+ except Exception as exc:
98
+ logger.warning("Failed to migrate secret %s: %s", key, exc)
99
+ result.failed.append((key, str(exc)))
100
+
101
+ return result