@trac3r/oh-my-god 2.2.11

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 (638) hide show
  1. package/CHANGELOG.md +188 -0
  2. package/INSTALL-VERIFICATION-INDEX.md +51 -0
  3. package/LICENSE +21 -0
  4. package/OMG-setup.sh +2549 -0
  5. package/QUICK-REFERENCE.md +58 -0
  6. package/README.md +207 -0
  7. package/agents/__init__.py +1 -0
  8. package/agents/__pycache__/model_roles.cpython-313.pyc +0 -0
  9. package/agents/_model_roles.yaml +26 -0
  10. package/agents/designer.md +67 -0
  11. package/agents/explore.md +60 -0
  12. package/agents/model_roles.py +196 -0
  13. package/agents/omg-api-builder.md +23 -0
  14. package/agents/omg-architect-mode.md +41 -0
  15. package/agents/omg-architect.md +13 -0
  16. package/agents/omg-backend-engineer.md +41 -0
  17. package/agents/omg-critic.md +16 -0
  18. package/agents/omg-database-engineer.md +41 -0
  19. package/agents/omg-escalation-router.md +17 -0
  20. package/agents/omg-executor.md +12 -0
  21. package/agents/omg-frontend-designer.md +41 -0
  22. package/agents/omg-implement-mode.md +49 -0
  23. package/agents/omg-infra-engineer.md +41 -0
  24. package/agents/omg-qa-tester.md +16 -0
  25. package/agents/omg-research-mode.md +41 -0
  26. package/agents/omg-security-auditor.md +41 -0
  27. package/agents/omg-testing-engineer.md +41 -0
  28. package/agents/plan.md +80 -0
  29. package/agents/quick_task.md +64 -0
  30. package/agents/reviewer.md +83 -0
  31. package/agents/task.md +71 -0
  32. package/bin/omg +41 -0
  33. package/commands/OMG:ai-commit.md +113 -0
  34. package/commands/OMG:api-twin.md +22 -0
  35. package/commands/OMG:arch.md +313 -0
  36. package/commands/OMG:browser.md +29 -0
  37. package/commands/OMG:ccg.md +22 -0
  38. package/commands/OMG:compat.md +57 -0
  39. package/commands/OMG:cost.md +181 -0
  40. package/commands/OMG:crazy.md +125 -0
  41. package/commands/OMG:create-agent.md +183 -0
  42. package/commands/OMG:deep-plan.md +18 -0
  43. package/commands/OMG:deps.md +248 -0
  44. package/commands/OMG:diagnose-plugins.md +33 -0
  45. package/commands/OMG:doctor.md +37 -0
  46. package/commands/OMG:domain-init.md +11 -0
  47. package/commands/OMG:escalate.md +52 -0
  48. package/commands/OMG:forge.md +103 -0
  49. package/commands/OMG:health-check.md +48 -0
  50. package/commands/OMG:init.md +134 -0
  51. package/commands/OMG:issue.md +56 -0
  52. package/commands/OMG:mode.md +44 -0
  53. package/commands/OMG:playwright.md +17 -0
  54. package/commands/OMG:preflight.md +26 -0
  55. package/commands/OMG:preset.md +49 -0
  56. package/commands/OMG:profile-review.md +58 -0
  57. package/commands/OMG:project-init.md +11 -0
  58. package/commands/OMG:ralph-start.md +43 -0
  59. package/commands/OMG:ralph-stop.md +23 -0
  60. package/commands/OMG:security-check.md +28 -0
  61. package/commands/OMG:session-branch.md +101 -0
  62. package/commands/OMG:session-fork.md +57 -0
  63. package/commands/OMG:session-merge.md +138 -0
  64. package/commands/OMG:setup.md +82 -0
  65. package/commands/OMG:ship.md +18 -0
  66. package/commands/OMG:stats.md +225 -0
  67. package/commands/OMG:teams.md +54 -0
  68. package/commands/OMG:theme.md +44 -0
  69. package/commands/OMG:validate.md +59 -0
  70. package/commands/__init__.py +1 -0
  71. package/docs/command-surface.md +55 -0
  72. package/docs/install/claude-code.md +53 -0
  73. package/docs/install/codex.md +45 -0
  74. package/docs/install/gemini.md +43 -0
  75. package/docs/install/github-action.md +81 -0
  76. package/docs/install/github-app-required-checks.md +107 -0
  77. package/docs/install/github-app.md +161 -0
  78. package/docs/install/kimi.md +43 -0
  79. package/docs/install/opencode.md +38 -0
  80. package/docs/proof.md +182 -0
  81. package/hooks/__init__.py +0 -0
  82. package/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  83. package/hooks/__pycache__/_agent_registry.cpython-313.pyc +0 -0
  84. package/hooks/__pycache__/_analytics.cpython-313.pyc +0 -0
  85. package/hooks/__pycache__/_budget.cpython-313.pyc +0 -0
  86. package/hooks/__pycache__/_common.cpython-313.pyc +0 -0
  87. package/hooks/__pycache__/_compression_optimizer.cpython-313.pyc +0 -0
  88. package/hooks/__pycache__/_cost_ledger.cpython-313.pyc +0 -0
  89. package/hooks/__pycache__/_learnings.cpython-313.pyc +0 -0
  90. package/hooks/__pycache__/_memory.cpython-313.pyc +0 -0
  91. package/hooks/__pycache__/_post_write.cpython-313.pyc +0 -0
  92. package/hooks/__pycache__/_protected_context.cpython-313.pyc +0 -0
  93. package/hooks/__pycache__/_token_counter.cpython-313.pyc +0 -0
  94. package/hooks/__pycache__/branch_manager.cpython-313.pyc +0 -0
  95. package/hooks/__pycache__/budget_governor.cpython-313.pyc +0 -0
  96. package/hooks/__pycache__/circuit-breaker.cpython-313.pyc +0 -0
  97. package/hooks/__pycache__/compression_feedback.cpython-313.pyc +0 -0
  98. package/hooks/__pycache__/config-guard.cpython-313.pyc +0 -0
  99. package/hooks/__pycache__/context_pressure.cpython-313.pyc +0 -0
  100. package/hooks/__pycache__/credential_store.cpython-313.pyc +0 -0
  101. package/hooks/__pycache__/fetch-rate-limits.cpython-313.pyc +0 -0
  102. package/hooks/__pycache__/firewall.cpython-313.pyc +0 -0
  103. package/hooks/__pycache__/hashline-formatter-bridge.cpython-313.pyc +0 -0
  104. package/hooks/__pycache__/hashline-injector.cpython-313.pyc +0 -0
  105. package/hooks/__pycache__/hashline-validator.cpython-313.pyc +0 -0
  106. package/hooks/__pycache__/idle-detector.cpython-313.pyc +0 -0
  107. package/hooks/__pycache__/instructions-loaded.cpython-313.pyc +0 -0
  108. package/hooks/__pycache__/intentgate-keyword-detector.cpython-313.pyc +0 -0
  109. package/hooks/__pycache__/magic-keyword-router.cpython-313.pyc +0 -0
  110. package/hooks/__pycache__/policy_engine.cpython-313.pyc +0 -0
  111. package/hooks/__pycache__/post-tool-failure.cpython-313.pyc +0 -0
  112. package/hooks/__pycache__/post-write.cpython-313.pyc +0 -0
  113. package/hooks/__pycache__/post_write.cpython-313.pyc +0 -0
  114. package/hooks/__pycache__/pre-compact.cpython-313.pyc +0 -0
  115. package/hooks/__pycache__/pre-tool-inject.cpython-313.pyc +0 -0
  116. package/hooks/__pycache__/prompt-enhancer.cpython-313.pyc +0 -0
  117. package/hooks/__pycache__/quality-runner.cpython-313.pyc +0 -0
  118. package/hooks/__pycache__/query.cpython-313.pyc +0 -0
  119. package/hooks/__pycache__/secret-guard.cpython-313.pyc +0 -0
  120. package/hooks/__pycache__/secret_audit.cpython-313.pyc +0 -0
  121. package/hooks/__pycache__/security_validators.cpython-313.pyc +0 -0
  122. package/hooks/__pycache__/session-end-capture.cpython-313.pyc +0 -0
  123. package/hooks/__pycache__/session-start.cpython-313.pyc +0 -0
  124. package/hooks/__pycache__/setup_wizard.cpython-313.pyc +0 -0
  125. package/hooks/__pycache__/shadow_manager.cpython-313.pyc +0 -0
  126. package/hooks/__pycache__/state_migration.cpython-313.pyc +0 -0
  127. package/hooks/__pycache__/stop-gate.cpython-313.pyc +0 -0
  128. package/hooks/__pycache__/stop_dispatcher.cpython-313.pyc +0 -0
  129. package/hooks/__pycache__/tdd-gate.cpython-313.pyc +0 -0
  130. package/hooks/__pycache__/terms-guard.cpython-313.pyc +0 -0
  131. package/hooks/__pycache__/test-validator.cpython-313.pyc +0 -0
  132. package/hooks/__pycache__/test_generator_hook.cpython-313.pyc +0 -0
  133. package/hooks/__pycache__/todo-state-tracker.cpython-313.pyc +0 -0
  134. package/hooks/__pycache__/tool-ledger.cpython-313.pyc +0 -0
  135. package/hooks/__pycache__/trust_review.cpython-313.pyc +0 -0
  136. package/hooks/__pycache__/user-prompt-submit.cpython-313.pyc +0 -0
  137. package/hooks/_agent_registry.py +481 -0
  138. package/hooks/_analytics.py +291 -0
  139. package/hooks/_budget.py +31 -0
  140. package/hooks/_common.py +761 -0
  141. package/hooks/_compression_optimizer.py +119 -0
  142. package/hooks/_cost_ledger.py +176 -0
  143. package/hooks/_learnings.py +126 -0
  144. package/hooks/_memory.py +103 -0
  145. package/hooks/_post_write.py +46 -0
  146. package/hooks/_protected_context.py +150 -0
  147. package/hooks/_token_counter.py +221 -0
  148. package/hooks/branch_manager.py +255 -0
  149. package/hooks/budget_governor.py +326 -0
  150. package/hooks/circuit-breaker.py +270 -0
  151. package/hooks/compression_feedback.py +254 -0
  152. package/hooks/config-guard.py +193 -0
  153. package/hooks/context_pressure.py +119 -0
  154. package/hooks/credential_store.py +970 -0
  155. package/hooks/fetch-rate-limits.py +212 -0
  156. package/hooks/firewall.py +323 -0
  157. package/hooks/hashline-formatter-bridge.py +224 -0
  158. package/hooks/hashline-injector.py +273 -0
  159. package/hooks/hashline-validator.py +216 -0
  160. package/hooks/idle-detector.py +97 -0
  161. package/hooks/instructions-loaded.py +26 -0
  162. package/hooks/intentgate-keyword-detector.py +200 -0
  163. package/hooks/magic-keyword-router.py +195 -0
  164. package/hooks/policy_engine.py +767 -0
  165. package/hooks/post-tool-failure.py +19 -0
  166. package/hooks/post-write.py +233 -0
  167. package/hooks/pre-compact.py +470 -0
  168. package/hooks/pre-tool-inject.py +98 -0
  169. package/hooks/prompt-enhancer.py +879 -0
  170. package/hooks/quality-runner.py +191 -0
  171. package/hooks/query.py +512 -0
  172. package/hooks/secret-guard.py +120 -0
  173. package/hooks/secret_audit.py +144 -0
  174. package/hooks/security_validators.py +93 -0
  175. package/hooks/session-end-capture.py +505 -0
  176. package/hooks/session-start.py +261 -0
  177. package/hooks/setup_wizard.py +1101 -0
  178. package/hooks/shadow_manager.py +476 -0
  179. package/hooks/state_migration.py +228 -0
  180. package/hooks/stop-gate.py +7 -0
  181. package/hooks/stop_dispatcher.py +1259 -0
  182. package/hooks/tdd-gate.py +10 -0
  183. package/hooks/terms-guard.py +98 -0
  184. package/hooks/test-validator.py +462 -0
  185. package/hooks/test_generator_hook.py +123 -0
  186. package/hooks/todo-state-tracker.py +114 -0
  187. package/hooks/tool-ledger.py +165 -0
  188. package/hooks/trust_review.py +662 -0
  189. package/hooks/user-prompt-submit.py +12 -0
  190. package/hud/omg-hud.mjs +1571 -0
  191. package/lab/__init__.py +1 -0
  192. package/lab/__pycache__/__init__.cpython-313.pyc +0 -0
  193. package/lab/__pycache__/axolotl_adapter.cpython-313.pyc +0 -0
  194. package/lab/__pycache__/forge_runner.cpython-313.pyc +0 -0
  195. package/lab/__pycache__/gazebo_adapter.cpython-313.pyc +0 -0
  196. package/lab/__pycache__/isaac_gym_adapter.cpython-313.pyc +0 -0
  197. package/lab/__pycache__/mock_isaac_env.cpython-313.pyc +0 -0
  198. package/lab/__pycache__/pipeline.cpython-313.pyc +0 -0
  199. package/lab/__pycache__/policies.cpython-313.pyc +0 -0
  200. package/lab/__pycache__/pybullet_adapter.cpython-313.pyc +0 -0
  201. package/lab/axolotl_adapter.py +531 -0
  202. package/lab/forge_runner.py +103 -0
  203. package/lab/gazebo_adapter.py +168 -0
  204. package/lab/isaac_gym_adapter.py +190 -0
  205. package/lab/mock_isaac_env.py +47 -0
  206. package/lab/pipeline.py +712 -0
  207. package/lab/policies.py +52 -0
  208. package/lab/pybullet_adapter.py +192 -0
  209. package/package.json +61 -0
  210. package/plugins/README.md +78 -0
  211. package/plugins/__init__.py +1 -0
  212. package/plugins/__pycache__/__init__.cpython-313.pyc +0 -0
  213. package/plugins/advanced/commands/OMG-code-review.md +114 -0
  214. package/plugins/advanced/commands/OMG-deep-plan.md +266 -0
  215. package/plugins/advanced/commands/OMG-handoff.md +115 -0
  216. package/plugins/advanced/commands/OMG-learn.md +110 -0
  217. package/plugins/advanced/commands/OMG-maintainer.md +31 -0
  218. package/plugins/advanced/commands/OMG-ralph-start.md +43 -0
  219. package/plugins/advanced/commands/OMG-ralph-stop.md +23 -0
  220. package/plugins/advanced/commands/OMG-security-review.md +16 -0
  221. package/plugins/advanced/commands/OMG-sequential-thinking.md +20 -0
  222. package/plugins/advanced/commands/OMG-ship.md +46 -0
  223. package/plugins/advanced/commands/OMG:code-review.md +114 -0
  224. package/plugins/advanced/commands/OMG:deep-plan.md +266 -0
  225. package/plugins/advanced/commands/OMG:handoff.md +115 -0
  226. package/plugins/advanced/commands/OMG:learn.md +110 -0
  227. package/plugins/advanced/commands/OMG:maintainer.md +31 -0
  228. package/plugins/advanced/commands/OMG:ralph-start.md +43 -0
  229. package/plugins/advanced/commands/OMG:ralph-stop.md +23 -0
  230. package/plugins/advanced/commands/OMG:security-review.md +16 -0
  231. package/plugins/advanced/commands/OMG:sequential-thinking.md +20 -0
  232. package/plugins/advanced/commands/OMG:ship.md +46 -0
  233. package/plugins/advanced/plugin.json +104 -0
  234. package/plugins/core/plugin.json +204 -0
  235. package/plugins/dephealth/__init__.py +0 -0
  236. package/plugins/dephealth/__pycache__/__init__.cpython-313.pyc +0 -0
  237. package/plugins/dephealth/__pycache__/cve_scanner.cpython-313.pyc +0 -0
  238. package/plugins/dephealth/__pycache__/license_checker.cpython-313.pyc +0 -0
  239. package/plugins/dephealth/__pycache__/manifest_detector.cpython-313.pyc +0 -0
  240. package/plugins/dephealth/__pycache__/vuln_analyzer.cpython-313.pyc +0 -0
  241. package/plugins/dephealth/cve_scanner.py +279 -0
  242. package/plugins/dephealth/license_checker.py +135 -0
  243. package/plugins/dephealth/manifest_detector.py +423 -0
  244. package/plugins/dephealth/vuln_analyzer.py +176 -0
  245. package/plugins/testgen/__init__.py +0 -0
  246. package/plugins/testgen/__pycache__/__init__.cpython-313.pyc +0 -0
  247. package/plugins/testgen/__pycache__/codamosa_engine.cpython-313.pyc +0 -0
  248. package/plugins/testgen/__pycache__/edge_case_synthesizer.cpython-313.pyc +0 -0
  249. package/plugins/testgen/__pycache__/framework_detector.cpython-313.pyc +0 -0
  250. package/plugins/testgen/__pycache__/skeleton_generator.cpython-313.pyc +0 -0
  251. package/plugins/testgen/codamosa_engine.py +402 -0
  252. package/plugins/testgen/edge_case_synthesizer.py +184 -0
  253. package/plugins/testgen/framework_detector.py +271 -0
  254. package/plugins/testgen/skeleton_generator.py +219 -0
  255. package/plugins/viz/__init__.py +0 -0
  256. package/plugins/viz/__pycache__/__init__.cpython-313.pyc +0 -0
  257. package/plugins/viz/__pycache__/ast_parser.cpython-313.pyc +0 -0
  258. package/plugins/viz/__pycache__/diagram_generator.cpython-313.pyc +0 -0
  259. package/plugins/viz/__pycache__/graph_builder.cpython-313.pyc +0 -0
  260. package/plugins/viz/__pycache__/native_parsers.cpython-313.pyc +0 -0
  261. package/plugins/viz/__pycache__/regex_parser.cpython-313.pyc +0 -0
  262. package/plugins/viz/ast_parser.py +139 -0
  263. package/plugins/viz/diagram_generator.py +192 -0
  264. package/plugins/viz/graph_builder.py +444 -0
  265. package/plugins/viz/native_parsers.py +259 -0
  266. package/plugins/viz/regex_parser.py +112 -0
  267. package/pyproject.toml +143 -0
  268. package/registry/__init__.py +1 -0
  269. package/registry/__pycache__/__init__.cpython-313.pyc +0 -0
  270. package/registry/__pycache__/approval_artifact.cpython-313.pyc +0 -0
  271. package/registry/__pycache__/verify_artifact.cpython-313.pyc +0 -0
  272. package/registry/approval_artifact.py +236 -0
  273. package/registry/bundles/algorithms.yaml +45 -0
  274. package/registry/bundles/api-twin.yaml +48 -0
  275. package/registry/bundles/ast-pack.yaml +80 -0
  276. package/registry/bundles/claim-judge.yaml +49 -0
  277. package/registry/bundles/control-plane.yaml +192 -0
  278. package/registry/bundles/data-lineage.yaml +47 -0
  279. package/registry/bundles/delta-classifier.yaml +47 -0
  280. package/registry/bundles/eval-gate.yaml +47 -0
  281. package/registry/bundles/hash-edit.yaml +73 -0
  282. package/registry/bundles/health.yaml +45 -0
  283. package/registry/bundles/hook-governor.yaml +101 -0
  284. package/registry/bundles/incident-replay.yaml +47 -0
  285. package/registry/bundles/lsp-pack.yaml +80 -0
  286. package/registry/bundles/mcp-fabric.yaml +53 -0
  287. package/registry/bundles/plan-council.yaml +56 -0
  288. package/registry/bundles/preflight.yaml +48 -0
  289. package/registry/bundles/proof-gate.yaml +49 -0
  290. package/registry/bundles/remote-supervisor.yaml +49 -0
  291. package/registry/bundles/robotics.yaml +45 -0
  292. package/registry/bundles/secure-worktree-pipeline.yaml +69 -0
  293. package/registry/bundles/security-check.yaml +50 -0
  294. package/registry/bundles/terminal-lane.yaml +61 -0
  295. package/registry/bundles/test-intent-lock.yaml +49 -0
  296. package/registry/bundles/tracebank.yaml +47 -0
  297. package/registry/bundles/vision.yaml +45 -0
  298. package/registry/omg-capability.schema.json +378 -0
  299. package/registry/policy-packs/airgapped.lock.json +11 -0
  300. package/registry/policy-packs/airgapped.signature.json +10 -0
  301. package/registry/policy-packs/airgapped.yaml +16 -0
  302. package/registry/policy-packs/fintech.lock.json +11 -0
  303. package/registry/policy-packs/fintech.signature.json +10 -0
  304. package/registry/policy-packs/fintech.yaml +15 -0
  305. package/registry/policy-packs/locked-prod.lock.json +11 -0
  306. package/registry/policy-packs/locked-prod.signature.json +10 -0
  307. package/registry/policy-packs/locked-prod.yaml +18 -0
  308. package/registry/trusted_signers.json +44 -0
  309. package/registry/verify_artifact.py +493 -0
  310. package/runtime/__init__.py +36 -0
  311. package/runtime/__pycache__/__init__.cpython-313.pyc +0 -0
  312. package/runtime/__pycache__/adoption.cpython-313.pyc +0 -0
  313. package/runtime/__pycache__/agent_selector.cpython-313.pyc +0 -0
  314. package/runtime/__pycache__/api_twin.cpython-313.pyc +0 -0
  315. package/runtime/__pycache__/architecture_signal.cpython-313.pyc +0 -0
  316. package/runtime/__pycache__/artifact_parsers.cpython-313.pyc +0 -0
  317. package/runtime/__pycache__/asset_loader.cpython-313.pyc +0 -0
  318. package/runtime/__pycache__/background_verification.cpython-313.pyc +0 -0
  319. package/runtime/__pycache__/budget_envelopes.cpython-313.pyc +0 -0
  320. package/runtime/__pycache__/business_workflow.cpython-313.pyc +0 -0
  321. package/runtime/__pycache__/canonical_surface.cpython-313.pyc +0 -0
  322. package/runtime/__pycache__/canonical_taxonomy.cpython-313.pyc +0 -0
  323. package/runtime/__pycache__/claim_judge.cpython-313.pyc +0 -0
  324. package/runtime/__pycache__/cli_provider.cpython-313.pyc +0 -0
  325. package/runtime/__pycache__/compat.cpython-313.pyc +0 -0
  326. package/runtime/__pycache__/complexity_scorer.cpython-313.pyc +0 -0
  327. package/runtime/__pycache__/compliance_governor.cpython-313.pyc +0 -0
  328. package/runtime/__pycache__/config_transaction.cpython-313.pyc +0 -0
  329. package/runtime/__pycache__/context_compiler.cpython-313.pyc +0 -0
  330. package/runtime/__pycache__/context_engine.cpython-313.pyc +0 -0
  331. package/runtime/__pycache__/context_limits.cpython-313.pyc +0 -0
  332. package/runtime/__pycache__/contract_compiler.cpython-313.pyc +0 -0
  333. package/runtime/__pycache__/custom_agent_loader.cpython-313.pyc +0 -0
  334. package/runtime/__pycache__/data_lineage.cpython-313.pyc +0 -0
  335. package/runtime/__pycache__/defense_state.cpython-313.pyc +0 -0
  336. package/runtime/__pycache__/delta_classifier.cpython-313.pyc +0 -0
  337. package/runtime/__pycache__/dispatcher.cpython-313.pyc +0 -0
  338. package/runtime/__pycache__/doc_generator.cpython-313.pyc +0 -0
  339. package/runtime/__pycache__/domain_packs.cpython-313.pyc +0 -0
  340. package/runtime/__pycache__/ecosystem.cpython-313.pyc +0 -0
  341. package/runtime/__pycache__/equalizer.cpython-313.pyc +0 -0
  342. package/runtime/__pycache__/eval_gate.cpython-313.pyc +0 -0
  343. package/runtime/__pycache__/evidence_narrator.cpython-313.pyc +0 -0
  344. package/runtime/__pycache__/evidence_query.cpython-313.pyc +0 -0
  345. package/runtime/__pycache__/evidence_registry.cpython-313.pyc +0 -0
  346. package/runtime/__pycache__/evidence_requirements.cpython-313.pyc +0 -0
  347. package/runtime/__pycache__/exec_kernel.cpython-313.pyc +0 -0
  348. package/runtime/__pycache__/explainer_formatter.cpython-313.pyc +0 -0
  349. package/runtime/__pycache__/feature_registry.cpython-313.pyc +0 -0
  350. package/runtime/__pycache__/forge_agents.cpython-313.pyc +0 -0
  351. package/runtime/__pycache__/forge_contracts.cpython-313.pyc +0 -0
  352. package/runtime/__pycache__/forge_domains.cpython-313.pyc +0 -0
  353. package/runtime/__pycache__/forge_run_id.cpython-313.pyc +0 -0
  354. package/runtime/__pycache__/github_integration.cpython-313.pyc +0 -0
  355. package/runtime/__pycache__/github_review_bot.cpython-313.pyc +0 -0
  356. package/runtime/__pycache__/github_review_contract.cpython-313.pyc +0 -0
  357. package/runtime/__pycache__/github_review_formatter.cpython-313.pyc +0 -0
  358. package/runtime/__pycache__/guide_assert.cpython-313.pyc +0 -0
  359. package/runtime/__pycache__/hook_governor.cpython-313.pyc +0 -0
  360. package/runtime/__pycache__/host_parity.cpython-313.pyc +0 -0
  361. package/runtime/__pycache__/incident_replay.cpython-313.pyc +0 -0
  362. package/runtime/__pycache__/install_planner.cpython-313.pyc +0 -0
  363. package/runtime/__pycache__/interaction_journal.cpython-313.pyc +0 -0
  364. package/runtime/__pycache__/issue_surface.cpython-313.pyc +0 -0
  365. package/runtime/__pycache__/legacy_compat.cpython-313.pyc +0 -0
  366. package/runtime/__pycache__/mcp_config_writers.cpython-313.pyc +0 -0
  367. package/runtime/__pycache__/mcp_lifecycle.cpython-313.pyc +0 -0
  368. package/runtime/__pycache__/mcp_memory_server.cpython-313.pyc +0 -0
  369. package/runtime/__pycache__/memory_store.cpython-313.pyc +0 -0
  370. package/runtime/__pycache__/merge_writer.cpython-313.pyc +0 -0
  371. package/runtime/__pycache__/music_omr_testbed.cpython-313.pyc +0 -0
  372. package/runtime/__pycache__/mutation_gate.cpython-313.pyc +0 -0
  373. package/runtime/__pycache__/omc_compat.cpython-313.pyc +0 -0
  374. package/runtime/__pycache__/omg_browser_cli.cpython-313.pyc +0 -0
  375. package/runtime/__pycache__/omg_mcp_server.cpython-313.pyc +0 -0
  376. package/runtime/__pycache__/opus_plan.cpython-313.pyc +0 -0
  377. package/runtime/__pycache__/playwright_adapter.cpython-313.pyc +0 -0
  378. package/runtime/__pycache__/playwright_pack.cpython-313.pyc +0 -0
  379. package/runtime/__pycache__/plugin_diagnostics.cpython-313.pyc +0 -0
  380. package/runtime/__pycache__/plugin_interop.cpython-313.pyc +0 -0
  381. package/runtime/__pycache__/policy_pack_loader.cpython-313.pyc +0 -0
  382. package/runtime/__pycache__/preflight.cpython-313.pyc +0 -0
  383. package/runtime/__pycache__/profile_io.cpython-313.pyc +0 -0
  384. package/runtime/__pycache__/prompt_compiler.cpython-313.pyc +0 -0
  385. package/runtime/__pycache__/proof_chain.cpython-313.pyc +0 -0
  386. package/runtime/__pycache__/proof_gate.cpython-313.pyc +0 -0
  387. package/runtime/__pycache__/provider_parity_eval.cpython-313.pyc +0 -0
  388. package/runtime/__pycache__/release_artifact_audit.cpython-313.pyc +0 -0
  389. package/runtime/__pycache__/release_run_coordinator.cpython-313.pyc +0 -0
  390. package/runtime/__pycache__/release_surface_compiler.cpython-313.pyc +0 -0
  391. package/runtime/__pycache__/release_surface_registry.cpython-313.pyc +0 -0
  392. package/runtime/__pycache__/release_surfaces.cpython-313.pyc +0 -0
  393. package/runtime/__pycache__/remote_supervisor.cpython-313.pyc +0 -0
  394. package/runtime/__pycache__/repro_pack.cpython-313.pyc +0 -0
  395. package/runtime/__pycache__/rollback_manifest.cpython-313.pyc +0 -0
  396. package/runtime/__pycache__/router_critics.cpython-313.pyc +0 -0
  397. package/runtime/__pycache__/router_executor.cpython-313.pyc +0 -0
  398. package/runtime/__pycache__/router_selector.cpython-313.pyc +0 -0
  399. package/runtime/__pycache__/runtime_contracts.cpython-313.pyc +0 -0
  400. package/runtime/__pycache__/runtime_profile.cpython-313.pyc +0 -0
  401. package/runtime/__pycache__/security_check.cpython-313.pyc +0 -0
  402. package/runtime/__pycache__/session_health.cpython-313.pyc +0 -0
  403. package/runtime/__pycache__/skill_evolution.cpython-313.pyc +0 -0
  404. package/runtime/__pycache__/skill_registry.cpython-313.pyc +0 -0
  405. package/runtime/__pycache__/subagent_dispatcher.cpython-313.pyc +0 -0
  406. package/runtime/__pycache__/subscription_tiers.cpython-313.pyc +0 -0
  407. package/runtime/__pycache__/team_router.cpython-313.pyc +0 -0
  408. package/runtime/__pycache__/test_intent_lock.cpython-313-pytest-9.0.2.pyc +0 -0
  409. package/runtime/__pycache__/test_intent_lock.cpython-313.pyc +0 -0
  410. package/runtime/__pycache__/tmux_session_manager.cpython-313.pyc +0 -0
  411. package/runtime/__pycache__/tool_fabric.cpython-313.pyc +0 -0
  412. package/runtime/__pycache__/tool_plan_gate.cpython-313.pyc +0 -0
  413. package/runtime/__pycache__/tool_relevance.cpython-313.pyc +0 -0
  414. package/runtime/__pycache__/tracebank.cpython-313.pyc +0 -0
  415. package/runtime/__pycache__/untrusted_content.cpython-313.pyc +0 -0
  416. package/runtime/__pycache__/validate.cpython-313.pyc +0 -0
  417. package/runtime/__pycache__/verdict_schema.cpython-313.pyc +0 -0
  418. package/runtime/__pycache__/verification_controller.cpython-313.pyc +0 -0
  419. package/runtime/__pycache__/verification_loop.cpython-313.pyc +0 -0
  420. package/runtime/__pycache__/vision_artifacts.cpython-313.pyc +0 -0
  421. package/runtime/__pycache__/vision_cache.cpython-313.pyc +0 -0
  422. package/runtime/__pycache__/vision_jobs.cpython-313.pyc +0 -0
  423. package/runtime/__pycache__/worker_watchdog.cpython-313.pyc +0 -0
  424. package/runtime/adapters/__init__.py +13 -0
  425. package/runtime/adapters/__pycache__/__init__.cpython-313.pyc +0 -0
  426. package/runtime/adapters/__pycache__/claude.cpython-313.pyc +0 -0
  427. package/runtime/adapters/__pycache__/gpt.cpython-313.pyc +0 -0
  428. package/runtime/adapters/__pycache__/local.cpython-313.pyc +0 -0
  429. package/runtime/adapters/claude.py +63 -0
  430. package/runtime/adapters/gpt.py +56 -0
  431. package/runtime/adapters/local.py +56 -0
  432. package/runtime/adoption.py +280 -0
  433. package/runtime/api_twin.py +450 -0
  434. package/runtime/architecture_signal.py +226 -0
  435. package/runtime/artifact_parsers.py +161 -0
  436. package/runtime/asset_loader.py +62 -0
  437. package/runtime/background_verification.py +178 -0
  438. package/runtime/budget_envelopes.py +398 -0
  439. package/runtime/business_workflow.py +234 -0
  440. package/runtime/canonical_surface.py +53 -0
  441. package/runtime/canonical_taxonomy.py +27 -0
  442. package/runtime/claim_judge.py +648 -0
  443. package/runtime/cli_provider.py +105 -0
  444. package/runtime/compat.py +2222 -0
  445. package/runtime/complexity_scorer.py +148 -0
  446. package/runtime/compliance_governor.py +505 -0
  447. package/runtime/config_transaction.py +304 -0
  448. package/runtime/context_compiler.py +131 -0
  449. package/runtime/context_engine.py +708 -0
  450. package/runtime/context_limits.py +363 -0
  451. package/runtime/contract_compiler.py +3664 -0
  452. package/runtime/custom_agent_loader.py +366 -0
  453. package/runtime/data_lineage.py +244 -0
  454. package/runtime/defense_state.py +261 -0
  455. package/runtime/delta_classifier.py +231 -0
  456. package/runtime/dispatcher.py +47 -0
  457. package/runtime/doc_generator.py +319 -0
  458. package/runtime/domain_packs.py +75 -0
  459. package/runtime/ecosystem.py +371 -0
  460. package/runtime/equalizer.py +268 -0
  461. package/runtime/eval_gate.py +96 -0
  462. package/runtime/evidence_narrator.py +147 -0
  463. package/runtime/evidence_query.py +303 -0
  464. package/runtime/evidence_registry.py +16 -0
  465. package/runtime/evidence_requirements.py +157 -0
  466. package/runtime/exec_kernel.py +267 -0
  467. package/runtime/explainer_formatter.py +82 -0
  468. package/runtime/feature_registry.py +109 -0
  469. package/runtime/forge_agents.py +915 -0
  470. package/runtime/forge_contracts.py +519 -0
  471. package/runtime/forge_domains.py +68 -0
  472. package/runtime/forge_run_id.py +86 -0
  473. package/runtime/guide_assert.py +135 -0
  474. package/runtime/hook_governor.py +156 -0
  475. package/runtime/host_parity.py +373 -0
  476. package/runtime/incident_replay.py +310 -0
  477. package/runtime/install_planner.py +617 -0
  478. package/runtime/interaction_journal.py +566 -0
  479. package/runtime/issue_surface.py +472 -0
  480. package/runtime/legacy_compat.py +7 -0
  481. package/runtime/mcp_config_writers.py +360 -0
  482. package/runtime/mcp_lifecycle.py +175 -0
  483. package/runtime/mcp_memory_server.py +220 -0
  484. package/runtime/memory_parsers/__init__.py +0 -0
  485. package/runtime/memory_parsers/__pycache__/__init__.cpython-313.pyc +0 -0
  486. package/runtime/memory_parsers/__pycache__/chatgpt_parser.cpython-313.pyc +0 -0
  487. package/runtime/memory_parsers/__pycache__/claude_import.cpython-313.pyc +0 -0
  488. package/runtime/memory_parsers/__pycache__/export.cpython-313.pyc +0 -0
  489. package/runtime/memory_parsers/__pycache__/gemini_import.cpython-313.pyc +0 -0
  490. package/runtime/memory_parsers/__pycache__/kimi_import.cpython-313.pyc +0 -0
  491. package/runtime/memory_parsers/chatgpt_parser.py +257 -0
  492. package/runtime/memory_parsers/claude_import.py +107 -0
  493. package/runtime/memory_parsers/export.py +97 -0
  494. package/runtime/memory_parsers/gemini_import.py +91 -0
  495. package/runtime/memory_parsers/kimi_import.py +91 -0
  496. package/runtime/memory_store.py +1182 -0
  497. package/runtime/merge_writer.py +445 -0
  498. package/runtime/music_omr_testbed.py +336 -0
  499. package/runtime/mutation_gate.py +320 -0
  500. package/runtime/omc_compat.py +7 -0
  501. package/runtime/omg_browser_cli.py +95 -0
  502. package/runtime/omg_compat_contract_snapshot.json +936 -0
  503. package/runtime/omg_contract_snapshot.json +936 -0
  504. package/runtime/omg_mcp_server.py +306 -0
  505. package/runtime/playwright_adapter.py +39 -0
  506. package/runtime/playwright_pack.py +253 -0
  507. package/runtime/plugin_diagnostics.py +308 -0
  508. package/runtime/plugin_interop.py +1060 -0
  509. package/runtime/policy_pack_loader.py +147 -0
  510. package/runtime/preflight.py +135 -0
  511. package/runtime/profile_io.py +328 -0
  512. package/runtime/proof_chain.py +472 -0
  513. package/runtime/proof_gate.py +442 -0
  514. package/runtime/provider_parity_eval.py +109 -0
  515. package/runtime/providers/__init__.py +0 -0
  516. package/runtime/providers/__pycache__/__init__.cpython-313.pyc +0 -0
  517. package/runtime/providers/__pycache__/codex_provider.cpython-313.pyc +0 -0
  518. package/runtime/providers/__pycache__/gemini_provider.cpython-313.pyc +0 -0
  519. package/runtime/providers/__pycache__/kimi_provider.cpython-313.pyc +0 -0
  520. package/runtime/providers/__pycache__/opencode_provider.cpython-313.pyc +0 -0
  521. package/runtime/providers/codex_provider.py +129 -0
  522. package/runtime/providers/gemini_provider.py +143 -0
  523. package/runtime/providers/kimi_provider.py +167 -0
  524. package/runtime/providers/opencode_provider.py +99 -0
  525. package/runtime/release_artifact_audit.py +556 -0
  526. package/runtime/release_run_coordinator.py +574 -0
  527. package/runtime/release_surface_compiler.py +643 -0
  528. package/runtime/release_surface_registry.py +283 -0
  529. package/runtime/release_surfaces.py +320 -0
  530. package/runtime/remote_supervisor.py +79 -0
  531. package/runtime/repro_pack.py +398 -0
  532. package/runtime/rollback_manifest.py +143 -0
  533. package/runtime/router_critics.py +229 -0
  534. package/runtime/router_executor.py +142 -0
  535. package/runtime/router_selector.py +99 -0
  536. package/runtime/runtime_contracts.py +292 -0
  537. package/runtime/runtime_profile.py +133 -0
  538. package/runtime/security_check.py +1094 -0
  539. package/runtime/session_health.py +546 -0
  540. package/runtime/skill_evolution.py +221 -0
  541. package/runtime/skill_registry.py +53 -0
  542. package/runtime/subagent_dispatcher.py +604 -0
  543. package/runtime/subscription_tiers.py +258 -0
  544. package/runtime/team_router.py +1399 -0
  545. package/runtime/test_intent_lock.py +543 -0
  546. package/runtime/tmux_session_manager.py +172 -0
  547. package/runtime/tool_fabric.py +570 -0
  548. package/runtime/tool_plan_gate.py +460 -0
  549. package/runtime/tracebank.py +125 -0
  550. package/runtime/untrusted_content.py +360 -0
  551. package/runtime/validate.py +293 -0
  552. package/runtime/verdict_schema.py +198 -0
  553. package/runtime/verification_controller.py +235 -0
  554. package/runtime/verification_loop.py +73 -0
  555. package/runtime/vision_artifacts.py +31 -0
  556. package/runtime/vision_cache.py +38 -0
  557. package/runtime/vision_jobs.py +92 -0
  558. package/runtime/worker_watchdog.py +526 -0
  559. package/scripts/__pycache__/audit-published-artifact.cpython-313.pyc +0 -0
  560. package/scripts/__pycache__/check-doc-parity.cpython-313.pyc +0 -0
  561. package/scripts/__pycache__/check-omg-standalone-clean.cpython-313.pyc +0 -0
  562. package/scripts/__pycache__/github_review_helpers.cpython-313.pyc +0 -0
  563. package/scripts/__pycache__/omg.cpython-313.pyc +0 -0
  564. package/scripts/__pycache__/prepare-release-proof-fixtures.cpython-313.pyc +0 -0
  565. package/scripts/__pycache__/sync-release-identity.cpython-313.pyc +0 -0
  566. package/scripts/__pycache__/validate-release-identity.cpython-313.pyc +0 -0
  567. package/scripts/audit-published-artifact.py +59 -0
  568. package/scripts/check-omg-compat-contract-snapshot.py +137 -0
  569. package/scripts/check-omg-contract-snapshot.py +12 -0
  570. package/scripts/check-omg-public-ready.py +273 -0
  571. package/scripts/check-omg-standalone-clean.py +133 -0
  572. package/scripts/emit_host_parity.py +72 -0
  573. package/scripts/legacy_to_omg_migrate.py +29 -0
  574. package/scripts/migrate-legacy.py +464 -0
  575. package/scripts/omc_to_omg_migrate.py +12 -0
  576. package/scripts/omg.py +2962 -0
  577. package/scripts/pre-release-check.sh +38 -0
  578. package/scripts/prepare-release-proof-fixtures.py +602 -0
  579. package/scripts/print-canonical-version.py +80 -0
  580. package/scripts/settings-merge.py +289 -0
  581. package/scripts/sync-release-identity.py +481 -0
  582. package/scripts/validate-release-identity.py +632 -0
  583. package/scripts/verify-no-omc.sh +5 -0
  584. package/scripts/verify-standalone.sh +35 -0
  585. package/settings.json +751 -0
  586. package/tools/__init__.py +2 -0
  587. package/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  588. package/tools/__pycache__/browser_consent.cpython-313.pyc +0 -0
  589. package/tools/__pycache__/browser_stealth.cpython-313.pyc +0 -0
  590. package/tools/__pycache__/browser_tool.cpython-313.pyc +0 -0
  591. package/tools/__pycache__/changelog_generator.cpython-313.pyc +0 -0
  592. package/tools/__pycache__/commit_splitter.cpython-313.pyc +0 -0
  593. package/tools/__pycache__/config_discovery.cpython-313.pyc +0 -0
  594. package/tools/__pycache__/config_merger.cpython-313.pyc +0 -0
  595. package/tools/__pycache__/dashboard_generator.cpython-313.pyc +0 -0
  596. package/tools/__pycache__/git_inspector.cpython-313.pyc +0 -0
  597. package/tools/__pycache__/lsp_client.cpython-313.pyc +0 -0
  598. package/tools/__pycache__/lsp_operations.cpython-313.pyc +0 -0
  599. package/tools/__pycache__/pr_generator.cpython-313.pyc +0 -0
  600. package/tools/__pycache__/python_repl.cpython-313.pyc +0 -0
  601. package/tools/__pycache__/python_sandbox.cpython-313.pyc +0 -0
  602. package/tools/__pycache__/session_snapshot.cpython-313.pyc +0 -0
  603. package/tools/__pycache__/ssh_manager.cpython-313.pyc +0 -0
  604. package/tools/__pycache__/theme_engine.cpython-313.pyc +0 -0
  605. package/tools/__pycache__/theme_selector.cpython-313.pyc +0 -0
  606. package/tools/__pycache__/web_search.cpython-313.pyc +0 -0
  607. package/tools/browser_consent.py +289 -0
  608. package/tools/browser_stealth.py +481 -0
  609. package/tools/browser_tool.py +448 -0
  610. package/tools/changelog_generator.py +347 -0
  611. package/tools/commit_splitter.py +749 -0
  612. package/tools/config_discovery.py +151 -0
  613. package/tools/config_merger.py +449 -0
  614. package/tools/dashboard_generator.py +300 -0
  615. package/tools/git_inspector.py +298 -0
  616. package/tools/lsp_client.py +275 -0
  617. package/tools/lsp_discovery.py +231 -0
  618. package/tools/lsp_operations.py +392 -0
  619. package/tools/pr_generator.py +404 -0
  620. package/tools/python_repl.py +712 -0
  621. package/tools/python_sandbox.py +768 -0
  622. package/tools/search_providers/__init__.py +77 -0
  623. package/tools/search_providers/__pycache__/__init__.cpython-313.pyc +0 -0
  624. package/tools/search_providers/__pycache__/brave.cpython-313.pyc +0 -0
  625. package/tools/search_providers/__pycache__/exa.cpython-313.pyc +0 -0
  626. package/tools/search_providers/__pycache__/jina.cpython-313.pyc +0 -0
  627. package/tools/search_providers/__pycache__/perplexity.cpython-313.pyc +0 -0
  628. package/tools/search_providers/__pycache__/synthetic.cpython-313.pyc +0 -0
  629. package/tools/search_providers/brave.py +115 -0
  630. package/tools/search_providers/exa.py +116 -0
  631. package/tools/search_providers/jina.py +104 -0
  632. package/tools/search_providers/perplexity.py +139 -0
  633. package/tools/search_providers/synthetic.py +74 -0
  634. package/tools/session_snapshot.py +851 -0
  635. package/tools/ssh_manager.py +912 -0
  636. package/tools/theme_engine.py +296 -0
  637. package/tools/theme_selector.py +137 -0
  638. package/tools/web_search.py +675 -0
@@ -0,0 +1,970 @@
1
+ #!/usr/bin/env python3
2
+ # pyright: reportConstantRedefinition=false, reportMissingTypeArgument=false
3
+ """OMG Multi-Credential Encrypted Store
4
+
5
+ Fernet-based encrypted credential storage with PBKDF2HMAC key derivation.
6
+ Stores encrypted credentials at .omg/state/credentials.enc with metadata
7
+ at .omg/state/credentials.meta.
8
+
9
+ CLI: python hooks/credential_store.py {add,list,remove,rotate} [options]
10
+
11
+ Feature flag: OMG_MULTI_CREDENTIAL_ENABLED (default off)
12
+ Design note: encrypted credentials live in OMG-managed state under .omg/state
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import base64
18
+ import gc
19
+ import getpass
20
+ import hashlib
21
+ import json
22
+ import os
23
+ import sys
24
+ from datetime import datetime, timezone
25
+ from typing import Any
26
+
27
+ HOOKS_DIR = os.path.dirname(os.path.abspath(__file__))
28
+ PROJECT_ROOT = os.path.dirname(HOOKS_DIR)
29
+ if PROJECT_ROOT not in sys.path:
30
+ sys.path.insert(0, PROJECT_ROOT)
31
+
32
+ from hooks._common import (
33
+ atomic_json_write,
34
+ get_feature_flag,
35
+ get_project_dir,
36
+ setup_crash_handler,
37
+ )
38
+
39
+ setup_crash_handler("credential_store", fail_closed=True)
40
+
41
+ # --- Lazy-loaded cryptography imports ---
42
+ _Fernet = None
43
+ _InvalidToken = None
44
+ _CRYPTO_BACKEND: str | None = None
45
+
46
+
47
+ def _ensure_crypto():
48
+ """Require cryptography/Fernet for credential-store encryption."""
49
+ global _Fernet, _InvalidToken, _CRYPTO_BACKEND
50
+ if _CRYPTO_BACKEND is not None:
51
+ if _CRYPTO_BACKEND != "fernet" or _Fernet is None or _InvalidToken is None:
52
+ raise RuntimeError("Secure credential backend unavailable: cryptography is required")
53
+ return
54
+ try:
55
+ from cryptography.fernet import Fernet, InvalidToken
56
+
57
+ _Fernet = Fernet
58
+ _InvalidToken = InvalidToken
59
+ _CRYPTO_BACKEND = "fernet"
60
+ except ImportError as exc:
61
+ _Fernet = None
62
+ _InvalidToken = None
63
+ _CRYPTO_BACKEND = "unavailable"
64
+ raise RuntimeError("Secure credential backend unavailable: cryptography is required") from exc
65
+
66
+
67
+ # --- Constants ---
68
+ CREDENTIALS_ENC = "credentials.enc"
69
+ CREDENTIALS_META = "credentials.meta"
70
+ STATE_DIR = os.path.join(".omg", "state")
71
+ KDF_ITERATIONS = 600_000
72
+ SALT_BYTES = 16
73
+ MIN_PASSPHRASE_LEN = 8
74
+
75
+ # Default empty store schema
76
+ _EMPTY_STORE = {"version": 1, "providers": {}}
77
+
78
+
79
+ # =============================================================================
80
+ # Core Crypto Functions
81
+ # =============================================================================
82
+
83
+
84
+ def derive_key(passphrase: bytes, salt: bytes, kdf_config: dict | None = None) -> bytes:
85
+ """Derive a 32-byte URL-safe key from passphrase using stdlib PBKDF2.
86
+
87
+ Args:
88
+ passphrase: Raw passphrase bytes
89
+ salt: 16-byte random salt
90
+ kdf_config: Optional dict with 'iterations' override
91
+
92
+ Returns:
93
+ URL-safe base64-encoded 32-byte key suitable for Fernet
94
+ """
95
+ iterations = KDF_ITERATIONS
96
+ if kdf_config and "iterations" in kdf_config:
97
+ iterations = int(kdf_config["iterations"])
98
+
99
+ derived = hashlib.pbkdf2_hmac("sha256", passphrase, salt, iterations, dklen=32)
100
+ return base64.urlsafe_b64encode(derived)
101
+
102
+
103
+ def encrypt_store(data: dict, key: bytes) -> bytes:
104
+ """Encrypt credential store payload with Fernet.
105
+
106
+ Args:
107
+ data: Credential store dict to encrypt
108
+ key: Derived key (from derive_key)
109
+
110
+ Returns:
111
+ Token bytes
112
+
113
+ Raises:
114
+ RuntimeError: If secure cryptography backend is unavailable
115
+ """
116
+ _ensure_crypto()
117
+ payload = json.dumps(data, separators=(",", ":")).encode("utf-8")
118
+ if _Fernet is None:
119
+ raise RuntimeError("Secure credential backend unavailable: cryptography is required")
120
+ return _Fernet(key).encrypt(payload)
121
+
122
+
123
+ def decrypt_store(token: bytes, key: bytes) -> dict:
124
+ """Decrypt credential store payload.
125
+
126
+ Args:
127
+ token: Fernet token bytes
128
+ key: Derived key (from derive_key)
129
+
130
+ Returns:
131
+ Decrypted credential store dict
132
+
133
+ Raises:
134
+ ValueError: If passphrase is wrong or store contents are corrupted
135
+ RuntimeError: If secure cryptography backend is unavailable
136
+ """
137
+ _ensure_crypto()
138
+ if _Fernet is None or _InvalidToken is None:
139
+ raise RuntimeError("Secure credential backend unavailable: cryptography is required")
140
+
141
+ f = _Fernet(key)
142
+ try:
143
+ plaintext = f.decrypt(token)
144
+ except _InvalidToken:
145
+ raise ValueError("Decryption failed: wrong passphrase or corrupted store")
146
+ return json.loads(plaintext.decode("utf-8"))
147
+
148
+
149
+ # =============================================================================
150
+ # Store I/O
151
+ # =============================================================================
152
+
153
+
154
+ def _get_store_paths(project_dir: str | None = None) -> tuple[str, str]:
155
+ """Return (enc_path, meta_path) for the credential store."""
156
+ pdir = project_dir or get_project_dir()
157
+ state_dir = os.path.join(pdir, STATE_DIR)
158
+ return (
159
+ os.path.join(state_dir, CREDENTIALS_ENC),
160
+ os.path.join(state_dir, CREDENTIALS_META),
161
+ )
162
+
163
+
164
+ def _load_meta(meta_path: str) -> dict:
165
+ """Load metadata file or return default."""
166
+ if not os.path.exists(meta_path):
167
+ return {}
168
+ try:
169
+ with open(meta_path, "r", encoding="utf-8") as f:
170
+ return json.load(f)
171
+ except (json.JSONDecodeError, OSError):
172
+ return {}
173
+
174
+
175
+ def _save_meta(meta_path: str, meta: dict) -> None:
176
+ """Save metadata via atomic write."""
177
+ atomic_json_write(meta_path, meta)
178
+
179
+
180
+ def _create_new_meta(salt: bytes) -> dict:
181
+ """Create initial metadata structure."""
182
+ return {
183
+ "version": 1,
184
+ "kdf": "pbkdf2-sha256",
185
+ "kdf_params": {
186
+ "iterations": KDF_ITERATIONS,
187
+ "salt_b64": base64.b64encode(salt).decode("ascii"),
188
+ },
189
+ "created_at": datetime.now(timezone.utc).isoformat(),
190
+ "updated_at": datetime.now(timezone.utc).isoformat(),
191
+ "providers": [],
192
+ }
193
+
194
+
195
+ def load_store(passphrase: str, project_dir: str | None = None) -> dict:
196
+ """Load and decrypt the credential store. Creates new if missing.
197
+
198
+ Args:
199
+ passphrase: User passphrase string
200
+ project_dir: Optional project directory override
201
+
202
+ Returns:
203
+ Decrypted store dict
204
+ """
205
+ enc_path, meta_path = _get_store_paths(project_dir)
206
+ passphrase_bytes = passphrase.encode("utf-8")
207
+
208
+ if not os.path.exists(enc_path):
209
+ # New store — return fresh empty (deep copy to avoid shared mutation)
210
+ return {"version": _EMPTY_STORE["version"], "providers": {}}
211
+
212
+ meta = _load_meta(meta_path)
213
+ if not meta:
214
+ raise ValueError("Metadata file missing or corrupted; cannot derive key")
215
+
216
+ salt = base64.b64decode(meta["kdf_params"]["salt_b64"])
217
+ kdf_config = meta.get("kdf_params", {})
218
+ key = derive_key(passphrase_bytes, salt, kdf_config)
219
+
220
+ with open(enc_path, "rb") as f:
221
+ token = f.read()
222
+
223
+ store = decrypt_store(token, key)
224
+
225
+ # Best-effort memory cleanup
226
+ del passphrase_bytes
227
+ del key
228
+ gc.collect()
229
+
230
+ return store
231
+
232
+
233
+ def save_store(data: dict, passphrase: str, project_dir: str | None = None) -> None:
234
+ """Encrypt and atomically write the credential store.
235
+
236
+ Args:
237
+ data: Credential store dict to save
238
+ passphrase: User passphrase string
239
+ project_dir: Optional project directory override
240
+ """
241
+ enc_path, meta_path = _get_store_paths(project_dir)
242
+ passphrase_bytes = passphrase.encode("utf-8")
243
+
244
+ # Ensure state directory exists
245
+ state_dir = os.path.dirname(enc_path)
246
+ os.makedirs(state_dir, exist_ok=True)
247
+
248
+ meta = _load_meta(meta_path)
249
+
250
+ if not meta:
251
+ # First save — create new salt and metadata
252
+ salt = os.urandom(SALT_BYTES)
253
+ meta = _create_new_meta(salt)
254
+ else:
255
+ salt = base64.b64decode(meta["kdf_params"]["salt_b64"])
256
+
257
+ kdf_config = meta.get("kdf_params", {})
258
+ key = derive_key(passphrase_bytes, salt, kdf_config)
259
+ token = encrypt_store(data, key)
260
+
261
+ # Atomic write for encrypted store (temp + rename)
262
+ tmp_path = enc_path + ".tmp"
263
+ with open(tmp_path, "wb") as f:
264
+ f.write(token)
265
+ os.rename(tmp_path, enc_path)
266
+
267
+ # Update metadata (provider list only — no keys)
268
+ meta["updated_at"] = datetime.now(timezone.utc).isoformat()
269
+ meta["providers"] = sorted(data.get("providers", {}).keys())
270
+ _save_meta(meta_path, meta)
271
+
272
+ # Best-effort memory cleanup
273
+ del passphrase_bytes
274
+ del key
275
+ del token
276
+ gc.collect()
277
+
278
+
279
+ # =============================================================================
280
+ # Credential Operations
281
+ # =============================================================================
282
+
283
+
284
+ def add_credential(
285
+ provider: str,
286
+ key: str,
287
+ passphrase: str,
288
+ label: str | None = None,
289
+ project_dir: str | None = None,
290
+ expires_at: str | None = None,
291
+ ) -> None:
292
+ """Add an API key for a provider.
293
+
294
+ Args:
295
+ provider: Provider name (lowercase, alphanumeric + hyphens)
296
+ key: API key value (NEVER logged)
297
+ passphrase: User passphrase
298
+ label: Optional human-readable label
299
+ project_dir: Optional project directory
300
+ expires_at: Optional ISO8601 expiry datetime string
301
+ """
302
+ store = load_store(passphrase, project_dir)
303
+
304
+ if "providers" not in store:
305
+ store["providers"] = {}
306
+
307
+ if provider not in store["providers"]:
308
+ store["providers"][provider] = {
309
+ "keys": [],
310
+ "active_index": 0,
311
+ "rotation_policy": "round-robin",
312
+ }
313
+
314
+ provider_data = store["providers"][provider]
315
+ existing_keys = provider_data["keys"]
316
+
317
+ # Duplicate detection: compare last 8 chars only (avoid logging full key)
318
+ key_suffix = key[-8:] if len(key) >= 8 else key
319
+ for i, existing in enumerate(existing_keys):
320
+ existing_suffix = existing["key"][-8:] if len(existing["key"]) >= 8 else existing["key"]
321
+ if existing_suffix == key_suffix:
322
+ print(
323
+ f"Warning: Key ending in ...{key_suffix[-4:]} may already exist at index {i} for {provider}",
324
+ file=sys.stderr,
325
+ )
326
+ break
327
+
328
+ index = len(existing_keys)
329
+ if label is None:
330
+ label = f"key-{index}"
331
+
332
+ key_entry = {
333
+ "key": key,
334
+ "label": label,
335
+ "added": datetime.now(timezone.utc).isoformat(),
336
+ "last_used": None,
337
+ "usage_count": 0,
338
+ }
339
+ if expires_at is not None:
340
+ key_entry["expires_at"] = expires_at
341
+
342
+ existing_keys.append(key_entry)
343
+
344
+ # First key sets active_index
345
+ if index == 0:
346
+ provider_data["active_index"] = 0
347
+
348
+ save_store(store, passphrase, project_dir)
349
+ print(f"Added key '{label}' for provider '{provider}' at index {index}")
350
+
351
+
352
+ def list_credentials(
353
+ passphrase: str | None = None,
354
+ provider_filter: str | None = None,
355
+ project_dir: str | None = None,
356
+ ) -> dict[str, int]:
357
+ """List providers and key metadata.
358
+
359
+ Without passphrase: reads metadata only (provider names).
360
+ With passphrase: shows labels and usage stats (never keys).
361
+
362
+ Args:
363
+ passphrase: Optional passphrase for detailed view
364
+ provider_filter: Optional provider name to filter
365
+ project_dir: Optional project directory
366
+
367
+ Returns:
368
+ Dict of provider name → key count
369
+ """
370
+ _, meta_path = _get_store_paths(project_dir)
371
+ meta = _load_meta(meta_path)
372
+
373
+ if not meta or not meta.get("providers"):
374
+ print("No credentials configured.")
375
+ return {}
376
+
377
+ if passphrase and provider_filter:
378
+ # Detailed view for specific provider
379
+ store = load_store(passphrase, project_dir)
380
+ providers = store.get("providers", {})
381
+
382
+ if provider_filter not in providers:
383
+ print(f"Provider '{provider_filter}' not found.")
384
+ return {}
385
+
386
+ pdata = providers[provider_filter]
387
+ active_idx = pdata.get("active_index", 0)
388
+ policy = pdata.get("rotation_policy", "round-robin")
389
+ keys = pdata.get("keys", [])
390
+
391
+ print(f"Provider: {provider_filter} (rotation: {policy})")
392
+ for i, k in enumerate(keys):
393
+ active_marker = " [ACTIVE]" if i == active_idx else ""
394
+ last_used = k.get("last_used") or "never"
395
+ if last_used != "never":
396
+ last_used = last_used[:10] # Date only
397
+ added = (k.get("added") or "")[:10]
398
+ usage = k.get("usage_count", 0)
399
+ lbl = k.get("label", f"key-{i}")
400
+ print(f" [{i}] {lbl:<12} added={added} last_used={last_used} usage={usage}{active_marker}")
401
+
402
+ return {provider_filter: len(keys)}
403
+
404
+ # Summary view from metadata only
405
+ result = {}
406
+ if passphrase:
407
+ # Can decrypt to get key counts
408
+ store = load_store(passphrase, project_dir)
409
+ providers = store.get("providers", {})
410
+ for name in sorted(providers.keys()):
411
+ pdata = providers[name]
412
+ count = len(pdata.get("keys", []))
413
+ active = pdata.get("active_index", 0)
414
+ print(f"Provider: {name} ({count} keys, active: #{active})")
415
+ result[name] = count
416
+ else:
417
+ # Metadata only (no decryption)
418
+ for name in sorted(meta.get("providers", [])):
419
+ print(f"Provider: {name}")
420
+ result[name] = -1 # Count unknown without decryption
421
+
422
+ return result
423
+
424
+
425
+ def remove_credential(
426
+ provider: str,
427
+ index: int | None = None,
428
+ passphrase: str | None = None,
429
+ project_dir: str | None = None,
430
+ confirm: bool = True,
431
+ ) -> None:
432
+ """Remove a key or entire provider.
433
+
434
+ Args:
435
+ provider: Provider name
436
+ index: Key index to remove (None = remove entire provider)
437
+ passphrase: User passphrase
438
+ project_dir: Optional project directory
439
+ confirm: Whether to prompt for confirmation
440
+ """
441
+ if passphrase is None:
442
+ passphrase = _get_passphrase()
443
+
444
+ store = load_store(passphrase, project_dir)
445
+ providers = store.get("providers", {})
446
+
447
+ if provider not in providers:
448
+ print(f"Error: Provider '{provider}' not found.", file=sys.stderr)
449
+ sys.exit(1)
450
+
451
+ if index is not None:
452
+ # Remove specific key
453
+ keys = providers[provider].get("keys", [])
454
+ if index < 0 or index >= len(keys):
455
+ print(f"Error: Index {index} out of range (0-{len(keys) - 1}).", file=sys.stderr)
456
+ sys.exit(1)
457
+
458
+ lbl = keys[index].get("label", f"key-{index}")
459
+ if confirm:
460
+ answer = input(f"Remove key #{index} ('{lbl}') from {provider}? [y/N] ")
461
+ if answer.lower() not in ("y", "yes"):
462
+ print("Cancelled.")
463
+ return
464
+
465
+ keys.pop(index)
466
+
467
+ # Reset active_index if needed
468
+ active_idx = providers[provider].get("active_index", 0)
469
+ if active_idx >= len(keys):
470
+ providers[provider]["active_index"] = 0
471
+
472
+ if not keys:
473
+ # No keys left — remove entire provider
474
+ del providers[provider]
475
+ print(f"Removed last key from '{provider}'; provider removed.")
476
+ else:
477
+ print(f"Removed key #{index} ('{lbl}') from '{provider}'.")
478
+ else:
479
+ # Remove entire provider
480
+ key_count = len(providers[provider].get("keys", []))
481
+ if confirm:
482
+ answer = input(f"Remove provider '{provider}' ({key_count} keys)? [y/N] ")
483
+ if answer.lower() not in ("y", "yes"):
484
+ print("Cancelled.")
485
+ return
486
+
487
+ del providers[provider]
488
+ print(f"Removed provider '{provider}' ({key_count} keys).")
489
+
490
+ save_store(store, passphrase, project_dir)
491
+
492
+
493
+ def rotate_credential(
494
+ provider: str,
495
+ index: int | None = None,
496
+ strategy: str | None = None,
497
+ passphrase: str | None = None,
498
+ project_dir: str | None = None,
499
+ ) -> None:
500
+ """Rotate the active key for a provider.
501
+
502
+ Args:
503
+ provider: Provider name
504
+ index: Specific key index to set as active (None = advance to next)
505
+ strategy: New rotation strategy (round-robin|failover|manual)
506
+ passphrase: User passphrase
507
+ project_dir: Optional project directory
508
+ """
509
+ if passphrase is None:
510
+ passphrase = _get_passphrase()
511
+
512
+ store = load_store(passphrase, project_dir)
513
+ providers = store.get("providers", {})
514
+
515
+ if provider not in providers:
516
+ print(f"Error: Provider '{provider}' not found.", file=sys.stderr)
517
+ sys.exit(1)
518
+
519
+ pdata = providers[provider]
520
+ keys = pdata.get("keys", [])
521
+ if not keys:
522
+ print(f"Error: No keys configured for '{provider}'.", file=sys.stderr)
523
+ sys.exit(1)
524
+
525
+ if strategy is not None:
526
+ valid_strategies = ("round-robin", "failover", "manual")
527
+ if strategy not in valid_strategies:
528
+ print(f"Error: Invalid strategy '{strategy}'. Choose from: {', '.join(valid_strategies)}", file=sys.stderr)
529
+ sys.exit(1)
530
+ pdata["rotation_policy"] = strategy
531
+ print(f"Set rotation strategy for '{provider}' to '{strategy}'.")
532
+
533
+ if index is not None:
534
+ if index < 0 or index >= len(keys):
535
+ print(f"Error: Index {index} out of range (0-{len(keys) - 1}).", file=sys.stderr)
536
+ sys.exit(1)
537
+ pdata["active_index"] = index
538
+ lbl = keys[index].get("label", f"key-{index}")
539
+ print(f"Set active key for '{provider}' to #{index} ('{lbl}').")
540
+ elif strategy is None:
541
+ # Advance to next (round-robin style)
542
+ current = pdata.get("active_index", 0)
543
+ new_idx = (current + 1) % len(keys)
544
+ pdata["active_index"] = new_idx
545
+ lbl = keys[new_idx].get("label", f"key-{new_idx}")
546
+ print(f"Rotated '{provider}' active key to #{new_idx} ('{lbl}').")
547
+
548
+ save_store(store, passphrase, project_dir)
549
+
550
+
551
+ # =============================================================================
552
+ # Runtime API (called by team_router.py in Task 1.9)
553
+ # =============================================================================
554
+
555
+
556
+ def get_active_key(provider: str, project_dir: str | None = None) -> str | None:
557
+ """Get the currently active API key for a provider.
558
+
559
+ Called by runtime/team_router.py (Task 1.9).
560
+ Returns None if feature disabled, provider not found, or no passphrase.
561
+ """
562
+ if not get_feature_flag("MULTI_CREDENTIAL", default=False):
563
+ return None
564
+
565
+ passphrase = os.environ.get("OMG_CREDENTIAL_PASSPHRASE")
566
+ if not passphrase:
567
+ return None
568
+
569
+ try:
570
+ store = load_store(passphrase, project_dir)
571
+ except (ValueError, OSError, RuntimeError):
572
+ return None
573
+
574
+ providers = store.get("providers", {})
575
+ if provider not in providers:
576
+ return None
577
+
578
+ pdata = providers[provider]
579
+ keys = pdata.get("keys", [])
580
+ if not keys:
581
+ return None
582
+
583
+ active_idx = pdata.get("active_index", 0)
584
+ # Safety: clamp index
585
+ if active_idx < 0 or active_idx >= len(keys):
586
+ active_idx = 0
587
+
588
+ key_entry = keys[active_idx]
589
+
590
+ # Advisory expiry check — warn but NEVER block retrieval
591
+ try:
592
+ _warn_if_expired(provider, key_entry)
593
+ except Exception:
594
+ pass # Never let expiry check crash key retrieval
595
+
596
+ return key_entry.get("key")
597
+
598
+
599
+ def advance_key(provider: str, project_dir: str | None = None) -> None:
600
+ """Advance to next key for round-robin rotation.
601
+
602
+ Called after successful API call by team_router.py.
603
+ Updates usage_count and last_used on the current key before advancing.
604
+ """
605
+ if not get_feature_flag("MULTI_CREDENTIAL", default=False):
606
+ return
607
+
608
+ passphrase = os.environ.get("OMG_CREDENTIAL_PASSPHRASE")
609
+ if not passphrase:
610
+ return
611
+
612
+ try:
613
+ store = load_store(passphrase, project_dir)
614
+ except (ValueError, OSError, RuntimeError):
615
+ return
616
+
617
+ providers = store.get("providers", {})
618
+ if provider not in providers:
619
+ return
620
+
621
+ pdata = providers[provider]
622
+ keys = pdata.get("keys", [])
623
+ if len(keys) <= 1:
624
+ return # Nothing to rotate
625
+
626
+ policy = pdata.get("rotation_policy", "round-robin")
627
+ if policy == "manual":
628
+ return # Don't auto-advance for manual policy
629
+
630
+ active_idx = pdata.get("active_index", 0)
631
+ if 0 <= active_idx < len(keys):
632
+ keys[active_idx]["usage_count"] = keys[active_idx].get("usage_count", 0) + 1
633
+ keys[active_idx]["last_used"] = datetime.now(timezone.utc).isoformat()
634
+
635
+ if policy == "round-robin":
636
+ pdata["active_index"] = (active_idx + 1) % len(keys)
637
+
638
+ # Failover only advances on error, not after success
639
+ try:
640
+ save_store(store, passphrase, project_dir)
641
+ except (ValueError, OSError, RuntimeError):
642
+ pass # Best-effort; don't crash the API call
643
+
644
+
645
+ # =============================================================================
646
+ # Expiry & Rotation Schedule
647
+ # =============================================================================
648
+
649
+ # Default constants
650
+ _DEFAULT_ROTATION_SCHEDULE_DAYS = 90
651
+ _DEFAULT_EXPIRY_WARNING_DAYS = 14
652
+
653
+
654
+ def get_rotation_schedule_days() -> int:
655
+ """Get the configured rotation schedule in days.
656
+
657
+ Resolution order:
658
+ 1. settings.json → _omg.credentials.rotation_schedule_days
659
+ 2. Default: 90 days
660
+ """
661
+ try:
662
+ settings_path = os.path.join(get_project_dir(), "settings.json")
663
+ if os.path.exists(settings_path):
664
+ with open(settings_path, "r", encoding="utf-8") as f:
665
+ settings = json.load(f)
666
+ cred_cfg = settings.get("_omg", {}).get("credentials", {})
667
+ return int(cred_cfg.get("rotation_schedule_days", _DEFAULT_ROTATION_SCHEDULE_DAYS))
668
+ except (json.JSONDecodeError, OSError, TypeError, ValueError):
669
+ pass
670
+ return _DEFAULT_ROTATION_SCHEDULE_DAYS
671
+
672
+
673
+ def _get_expiry_warning_days() -> int:
674
+ """Get the configured expiry warning threshold in days (default: 14)."""
675
+ try:
676
+ settings_path = os.path.join(get_project_dir(), "settings.json")
677
+ if os.path.exists(settings_path):
678
+ with open(settings_path, "r", encoding="utf-8") as f:
679
+ settings = json.load(f)
680
+ cred_cfg = settings.get("_omg", {}).get("credentials", {})
681
+ return int(cred_cfg.get("expiry_warning_days", _DEFAULT_EXPIRY_WARNING_DAYS))
682
+ except (json.JSONDecodeError, OSError, TypeError, ValueError):
683
+ pass
684
+ return _DEFAULT_EXPIRY_WARNING_DAYS
685
+
686
+
687
+ def _parse_expiry(expires_at: str) -> datetime | None:
688
+ """Parse an ISO8601 expires_at string to datetime, or None on failure."""
689
+ try:
690
+ dt = datetime.fromisoformat(expires_at)
691
+ # Ensure timezone-aware
692
+ if dt.tzinfo is None:
693
+ dt = dt.replace(tzinfo=timezone.utc)
694
+ return dt
695
+ except (ValueError, TypeError):
696
+ return None
697
+
698
+
699
+ def check_expiry(project_dir: str) -> list[dict]:
700
+ """Check all credentials for expiry status.
701
+
702
+ Args:
703
+ project_dir: Project directory containing .omg/state/
704
+
705
+ Returns:
706
+ List of dicts with keys:
707
+ - name: provider name
708
+ - expires_at: ISO8601 string
709
+ - days_remaining: int (negative = already expired)
710
+ - status: 'expired' | 'expiring' | 'ok'
711
+
712
+ Credentials without expires_at are omitted from the report.
713
+ """
714
+ passphrase = os.environ.get("OMG_CREDENTIAL_PASSPHRASE")
715
+ if not passphrase:
716
+ return []
717
+
718
+ try:
719
+ store = load_store(passphrase, project_dir)
720
+ except (ValueError, OSError):
721
+ return []
722
+
723
+ providers = store.get("providers", {})
724
+ if not providers:
725
+ return []
726
+
727
+ now = datetime.now(timezone.utc)
728
+ warning_days = _DEFAULT_EXPIRY_WARNING_DAYS
729
+ try:
730
+ warning_days = _get_expiry_warning_days()
731
+ except Exception:
732
+ pass
733
+
734
+ results = []
735
+ for provider_name, pdata in sorted(providers.items()):
736
+ keys = pdata.get("keys", [])
737
+ active_idx = pdata.get("active_index", 0)
738
+
739
+ for i, key_entry in enumerate(keys):
740
+ expires_at_str = key_entry.get("expires_at")
741
+ if not expires_at_str:
742
+ continue
743
+
744
+ expiry_dt = _parse_expiry(expires_at_str)
745
+ if expiry_dt is None:
746
+ continue
747
+
748
+ delta = expiry_dt - now
749
+ days_remaining = int(delta.total_seconds() / 86400)
750
+
751
+ if days_remaining < 0:
752
+ status = "expired"
753
+ elif days_remaining <= warning_days:
754
+ status = "expiring"
755
+ else:
756
+ status = "ok"
757
+
758
+ label = key_entry.get("label", f"key-{i}")
759
+ results.append({
760
+ "name": provider_name,
761
+ "label": label,
762
+ "key_index": i,
763
+ "is_active": i == active_idx,
764
+ "expires_at": expires_at_str,
765
+ "days_remaining": days_remaining,
766
+ "status": status,
767
+ })
768
+
769
+ return results
770
+
771
+
772
+ def _warn_if_expired(provider: str, key_entry: dict) -> None:
773
+ """Print a warning to stderr if a key is expired or expiring. Advisory only."""
774
+ expires_at_str = key_entry.get("expires_at")
775
+ if not expires_at_str:
776
+ return
777
+
778
+ expiry_dt = _parse_expiry(expires_at_str)
779
+ if expiry_dt is None:
780
+ return
781
+
782
+ now = datetime.now(timezone.utc)
783
+ delta = expiry_dt - now
784
+ days_remaining = int(delta.total_seconds() / 86400)
785
+
786
+ if days_remaining < 0:
787
+ label = key_entry.get("label", "unknown")
788
+ print(
789
+ f"Warning: Key '{label}' for provider '{provider}' expired "
790
+ f"{abs(days_remaining)} days ago (expires_at: {expires_at_str})",
791
+ file=sys.stderr,
792
+ )
793
+ elif days_remaining <= _DEFAULT_EXPIRY_WARNING_DAYS:
794
+ label = key_entry.get("label", "unknown")
795
+ print(
796
+ f"Warning: Key '{label}' for provider '{provider}' expiring in "
797
+ f"{days_remaining} days (expires_at: {expires_at_str})",
798
+ file=sys.stderr,
799
+ )
800
+
801
+
802
+ # =============================================================================
803
+ # Passphrase Handling
804
+ # =============================================================================
805
+
806
+
807
+ def _get_passphrase() -> str:
808
+ """Get passphrase from env var or interactive prompt.
809
+
810
+ Resolution order:
811
+ 1. OMG_CREDENTIAL_PASSPHRASE env var
812
+ 2. getpass.getpass() interactive prompt (if TTY)
813
+ """
814
+ env_passphrase = os.environ.get("OMG_CREDENTIAL_PASSPHRASE")
815
+ if env_passphrase:
816
+ return env_passphrase
817
+
818
+ if not sys.stdin.isatty():
819
+ print(
820
+ "Error: No passphrase available. Set OMG_CREDENTIAL_PASSPHRASE env var "
821
+ "for non-interactive use.",
822
+ file=sys.stderr,
823
+ )
824
+ sys.exit(1)
825
+
826
+ passphrase = getpass.getpass("Credential store passphrase: ")
827
+ if len(passphrase) < MIN_PASSPHRASE_LEN:
828
+ print(
829
+ f"Warning: Passphrase is short ({len(passphrase)} chars). "
830
+ f"Recommended minimum: {MIN_PASSPHRASE_LEN} chars.",
831
+ file=sys.stderr,
832
+ )
833
+ return passphrase
834
+
835
+
836
+ # =============================================================================
837
+ # Feature Flag Gate
838
+ # =============================================================================
839
+
840
+
841
+ def _check_feature_flag() -> None:
842
+ """Verify the multi-credential feature flag is enabled."""
843
+ if not get_feature_flag("MULTI_CREDENTIAL", default=False):
844
+ print(
845
+ "Error: Multi-credential store is disabled.\n"
846
+ "Set OMG_MULTI_CREDENTIAL_ENABLED=1 to enable.",
847
+ file=sys.stderr,
848
+ )
849
+ sys.exit(1)
850
+
851
+
852
+ # =============================================================================
853
+ # CLI Interface
854
+ # =============================================================================
855
+
856
+
857
+ def _build_parser() -> argparse.ArgumentParser:
858
+ """Build the CLI argument parser."""
859
+ parser = argparse.ArgumentParser(
860
+ prog="omg-creds",
861
+ description="Multi-credential encrypted store for OMG.",
862
+ epilog=(
863
+ "environment:\n"
864
+ " OMG_MULTI_CREDENTIAL_ENABLED=1 Required to enable credential store\n"
865
+ " OMG_CREDENTIAL_PASSPHRASE Passphrase for non-interactive use\n"
866
+ "\n"
867
+ "examples:\n"
868
+ " %(prog)s add --provider anthropic --key sk-ant-xxx\n"
869
+ " %(prog)s add --provider openai --key sk-proj-xxx --label backup\n"
870
+ " %(prog)s list\n"
871
+ " %(prog)s list --provider anthropic\n"
872
+ " %(prog)s remove --provider anthropic --index 1\n"
873
+ " %(prog)s rotate --provider anthropic\n"
874
+ " %(prog)s rotate --provider openai --strategy failover"
875
+ ),
876
+ formatter_class=argparse.RawDescriptionHelpFormatter,
877
+ )
878
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
879
+
880
+ # add
881
+ add_p = subparsers.add_parser("add", help="Add an API key for a provider")
882
+ add_p.add_argument("--provider", required=True, help="Provider name (e.g., anthropic, openai)")
883
+ add_p.add_argument("--key", required=True, help="API key value")
884
+ add_p.add_argument("--label", default=None, help="Human-readable label (default: key-N)")
885
+
886
+ # list
887
+ list_p = subparsers.add_parser("list", help="List providers and key metadata")
888
+ list_p.add_argument("--provider", default=None, help="Filter to specific provider (requires passphrase)")
889
+
890
+ # remove
891
+ rm_p = subparsers.add_parser("remove", help="Remove a key or provider")
892
+ rm_p.add_argument("--provider", required=True, help="Provider name")
893
+ rm_p.add_argument("--index", type=int, default=None, help="Key index to remove (omit to remove entire provider)")
894
+ rm_p.add_argument("--yes", "-y", action="store_true", help="Skip confirmation prompt")
895
+
896
+ # rotate
897
+ rot_p = subparsers.add_parser("rotate", help="Rotate active key or set rotation strategy")
898
+ rot_p.add_argument("--provider", required=True, help="Provider name")
899
+ rot_p.add_argument("--index", type=int, default=None, help="Set specific key index as active")
900
+ rot_p.add_argument("--strategy", default=None, choices=["round-robin", "failover", "manual"], help="Set rotation strategy")
901
+
902
+ return parser
903
+
904
+
905
+ def main() -> None:
906
+ """CLI entry point."""
907
+ parser = _build_parser()
908
+ args = parser.parse_args()
909
+
910
+ if not args.command:
911
+ parser.print_help()
912
+ sys.exit(0)
913
+
914
+ # Feature flag gate
915
+ _check_feature_flag()
916
+
917
+ if args.command == "add":
918
+ passphrase = _get_passphrase()
919
+ add_credential(
920
+ provider=args.provider.lower().strip(),
921
+ key=args.key,
922
+ passphrase=passphrase,
923
+ label=args.label,
924
+ )
925
+ # Best-effort cleanup
926
+ del passphrase
927
+ gc.collect()
928
+
929
+ elif args.command == "list":
930
+ if args.provider:
931
+ passphrase = _get_passphrase()
932
+ list_credentials(
933
+ passphrase=passphrase,
934
+ provider_filter=args.provider.lower().strip(),
935
+ )
936
+ del passphrase
937
+ gc.collect()
938
+ else:
939
+ # Try without passphrase first (metadata only)
940
+ list_credentials(passphrase=None)
941
+
942
+ elif args.command == "remove":
943
+ passphrase = _get_passphrase()
944
+ remove_credential(
945
+ provider=args.provider.lower().strip(),
946
+ index=args.index,
947
+ passphrase=passphrase,
948
+ confirm=not args.yes,
949
+ )
950
+ del passphrase
951
+ gc.collect()
952
+
953
+ elif args.command == "rotate":
954
+ passphrase = _get_passphrase()
955
+ rotate_credential(
956
+ provider=args.provider.lower().strip(),
957
+ index=args.index,
958
+ strategy=args.strategy,
959
+ passphrase=passphrase,
960
+ )
961
+ del passphrase
962
+ gc.collect()
963
+
964
+ else:
965
+ parser.print_help()
966
+ sys.exit(1)
967
+
968
+
969
+ if __name__ == "__main__":
970
+ main()