@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.
- package/CHANGELOG.md +188 -0
- package/INSTALL-VERIFICATION-INDEX.md +51 -0
- package/LICENSE +21 -0
- package/OMG-setup.sh +2549 -0
- package/QUICK-REFERENCE.md +58 -0
- package/README.md +207 -0
- package/agents/__init__.py +1 -0
- package/agents/__pycache__/model_roles.cpython-313.pyc +0 -0
- package/agents/_model_roles.yaml +26 -0
- package/agents/designer.md +67 -0
- package/agents/explore.md +60 -0
- package/agents/model_roles.py +196 -0
- package/agents/omg-api-builder.md +23 -0
- package/agents/omg-architect-mode.md +41 -0
- package/agents/omg-architect.md +13 -0
- package/agents/omg-backend-engineer.md +41 -0
- package/agents/omg-critic.md +16 -0
- package/agents/omg-database-engineer.md +41 -0
- package/agents/omg-escalation-router.md +17 -0
- package/agents/omg-executor.md +12 -0
- package/agents/omg-frontend-designer.md +41 -0
- package/agents/omg-implement-mode.md +49 -0
- package/agents/omg-infra-engineer.md +41 -0
- package/agents/omg-qa-tester.md +16 -0
- package/agents/omg-research-mode.md +41 -0
- package/agents/omg-security-auditor.md +41 -0
- package/agents/omg-testing-engineer.md +41 -0
- package/agents/plan.md +80 -0
- package/agents/quick_task.md +64 -0
- package/agents/reviewer.md +83 -0
- package/agents/task.md +71 -0
- package/bin/omg +41 -0
- package/commands/OMG:ai-commit.md +113 -0
- package/commands/OMG:api-twin.md +22 -0
- package/commands/OMG:arch.md +313 -0
- package/commands/OMG:browser.md +29 -0
- package/commands/OMG:ccg.md +22 -0
- package/commands/OMG:compat.md +57 -0
- package/commands/OMG:cost.md +181 -0
- package/commands/OMG:crazy.md +125 -0
- package/commands/OMG:create-agent.md +183 -0
- package/commands/OMG:deep-plan.md +18 -0
- package/commands/OMG:deps.md +248 -0
- package/commands/OMG:diagnose-plugins.md +33 -0
- package/commands/OMG:doctor.md +37 -0
- package/commands/OMG:domain-init.md +11 -0
- package/commands/OMG:escalate.md +52 -0
- package/commands/OMG:forge.md +103 -0
- package/commands/OMG:health-check.md +48 -0
- package/commands/OMG:init.md +134 -0
- package/commands/OMG:issue.md +56 -0
- package/commands/OMG:mode.md +44 -0
- package/commands/OMG:playwright.md +17 -0
- package/commands/OMG:preflight.md +26 -0
- package/commands/OMG:preset.md +49 -0
- package/commands/OMG:profile-review.md +58 -0
- package/commands/OMG:project-init.md +11 -0
- package/commands/OMG:ralph-start.md +43 -0
- package/commands/OMG:ralph-stop.md +23 -0
- package/commands/OMG:security-check.md +28 -0
- package/commands/OMG:session-branch.md +101 -0
- package/commands/OMG:session-fork.md +57 -0
- package/commands/OMG:session-merge.md +138 -0
- package/commands/OMG:setup.md +82 -0
- package/commands/OMG:ship.md +18 -0
- package/commands/OMG:stats.md +225 -0
- package/commands/OMG:teams.md +54 -0
- package/commands/OMG:theme.md +44 -0
- package/commands/OMG:validate.md +59 -0
- package/commands/__init__.py +1 -0
- package/docs/command-surface.md +55 -0
- package/docs/install/claude-code.md +53 -0
- package/docs/install/codex.md +45 -0
- package/docs/install/gemini.md +43 -0
- package/docs/install/github-action.md +81 -0
- package/docs/install/github-app-required-checks.md +107 -0
- package/docs/install/github-app.md +161 -0
- package/docs/install/kimi.md +43 -0
- package/docs/install/opencode.md +38 -0
- package/docs/proof.md +182 -0
- package/hooks/__init__.py +0 -0
- package/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
- package/hooks/__pycache__/_agent_registry.cpython-313.pyc +0 -0
- package/hooks/__pycache__/_analytics.cpython-313.pyc +0 -0
- package/hooks/__pycache__/_budget.cpython-313.pyc +0 -0
- package/hooks/__pycache__/_common.cpython-313.pyc +0 -0
- package/hooks/__pycache__/_compression_optimizer.cpython-313.pyc +0 -0
- package/hooks/__pycache__/_cost_ledger.cpython-313.pyc +0 -0
- package/hooks/__pycache__/_learnings.cpython-313.pyc +0 -0
- package/hooks/__pycache__/_memory.cpython-313.pyc +0 -0
- package/hooks/__pycache__/_post_write.cpython-313.pyc +0 -0
- package/hooks/__pycache__/_protected_context.cpython-313.pyc +0 -0
- package/hooks/__pycache__/_token_counter.cpython-313.pyc +0 -0
- package/hooks/__pycache__/branch_manager.cpython-313.pyc +0 -0
- package/hooks/__pycache__/budget_governor.cpython-313.pyc +0 -0
- package/hooks/__pycache__/circuit-breaker.cpython-313.pyc +0 -0
- package/hooks/__pycache__/compression_feedback.cpython-313.pyc +0 -0
- package/hooks/__pycache__/config-guard.cpython-313.pyc +0 -0
- package/hooks/__pycache__/context_pressure.cpython-313.pyc +0 -0
- package/hooks/__pycache__/credential_store.cpython-313.pyc +0 -0
- package/hooks/__pycache__/fetch-rate-limits.cpython-313.pyc +0 -0
- package/hooks/__pycache__/firewall.cpython-313.pyc +0 -0
- package/hooks/__pycache__/hashline-formatter-bridge.cpython-313.pyc +0 -0
- package/hooks/__pycache__/hashline-injector.cpython-313.pyc +0 -0
- package/hooks/__pycache__/hashline-validator.cpython-313.pyc +0 -0
- package/hooks/__pycache__/idle-detector.cpython-313.pyc +0 -0
- package/hooks/__pycache__/instructions-loaded.cpython-313.pyc +0 -0
- package/hooks/__pycache__/intentgate-keyword-detector.cpython-313.pyc +0 -0
- package/hooks/__pycache__/magic-keyword-router.cpython-313.pyc +0 -0
- package/hooks/__pycache__/policy_engine.cpython-313.pyc +0 -0
- package/hooks/__pycache__/post-tool-failure.cpython-313.pyc +0 -0
- package/hooks/__pycache__/post-write.cpython-313.pyc +0 -0
- package/hooks/__pycache__/post_write.cpython-313.pyc +0 -0
- package/hooks/__pycache__/pre-compact.cpython-313.pyc +0 -0
- package/hooks/__pycache__/pre-tool-inject.cpython-313.pyc +0 -0
- package/hooks/__pycache__/prompt-enhancer.cpython-313.pyc +0 -0
- package/hooks/__pycache__/quality-runner.cpython-313.pyc +0 -0
- package/hooks/__pycache__/query.cpython-313.pyc +0 -0
- package/hooks/__pycache__/secret-guard.cpython-313.pyc +0 -0
- package/hooks/__pycache__/secret_audit.cpython-313.pyc +0 -0
- package/hooks/__pycache__/security_validators.cpython-313.pyc +0 -0
- package/hooks/__pycache__/session-end-capture.cpython-313.pyc +0 -0
- package/hooks/__pycache__/session-start.cpython-313.pyc +0 -0
- package/hooks/__pycache__/setup_wizard.cpython-313.pyc +0 -0
- package/hooks/__pycache__/shadow_manager.cpython-313.pyc +0 -0
- package/hooks/__pycache__/state_migration.cpython-313.pyc +0 -0
- package/hooks/__pycache__/stop-gate.cpython-313.pyc +0 -0
- package/hooks/__pycache__/stop_dispatcher.cpython-313.pyc +0 -0
- package/hooks/__pycache__/tdd-gate.cpython-313.pyc +0 -0
- package/hooks/__pycache__/terms-guard.cpython-313.pyc +0 -0
- package/hooks/__pycache__/test-validator.cpython-313.pyc +0 -0
- package/hooks/__pycache__/test_generator_hook.cpython-313.pyc +0 -0
- package/hooks/__pycache__/todo-state-tracker.cpython-313.pyc +0 -0
- package/hooks/__pycache__/tool-ledger.cpython-313.pyc +0 -0
- package/hooks/__pycache__/trust_review.cpython-313.pyc +0 -0
- package/hooks/__pycache__/user-prompt-submit.cpython-313.pyc +0 -0
- package/hooks/_agent_registry.py +481 -0
- package/hooks/_analytics.py +291 -0
- package/hooks/_budget.py +31 -0
- package/hooks/_common.py +761 -0
- package/hooks/_compression_optimizer.py +119 -0
- package/hooks/_cost_ledger.py +176 -0
- package/hooks/_learnings.py +126 -0
- package/hooks/_memory.py +103 -0
- package/hooks/_post_write.py +46 -0
- package/hooks/_protected_context.py +150 -0
- package/hooks/_token_counter.py +221 -0
- package/hooks/branch_manager.py +255 -0
- package/hooks/budget_governor.py +326 -0
- package/hooks/circuit-breaker.py +270 -0
- package/hooks/compression_feedback.py +254 -0
- package/hooks/config-guard.py +193 -0
- package/hooks/context_pressure.py +119 -0
- package/hooks/credential_store.py +970 -0
- package/hooks/fetch-rate-limits.py +212 -0
- package/hooks/firewall.py +323 -0
- package/hooks/hashline-formatter-bridge.py +224 -0
- package/hooks/hashline-injector.py +273 -0
- package/hooks/hashline-validator.py +216 -0
- package/hooks/idle-detector.py +97 -0
- package/hooks/instructions-loaded.py +26 -0
- package/hooks/intentgate-keyword-detector.py +200 -0
- package/hooks/magic-keyword-router.py +195 -0
- package/hooks/policy_engine.py +767 -0
- package/hooks/post-tool-failure.py +19 -0
- package/hooks/post-write.py +233 -0
- package/hooks/pre-compact.py +470 -0
- package/hooks/pre-tool-inject.py +98 -0
- package/hooks/prompt-enhancer.py +879 -0
- package/hooks/quality-runner.py +191 -0
- package/hooks/query.py +512 -0
- package/hooks/secret-guard.py +120 -0
- package/hooks/secret_audit.py +144 -0
- package/hooks/security_validators.py +93 -0
- package/hooks/session-end-capture.py +505 -0
- package/hooks/session-start.py +261 -0
- package/hooks/setup_wizard.py +1101 -0
- package/hooks/shadow_manager.py +476 -0
- package/hooks/state_migration.py +228 -0
- package/hooks/stop-gate.py +7 -0
- package/hooks/stop_dispatcher.py +1259 -0
- package/hooks/tdd-gate.py +10 -0
- package/hooks/terms-guard.py +98 -0
- package/hooks/test-validator.py +462 -0
- package/hooks/test_generator_hook.py +123 -0
- package/hooks/todo-state-tracker.py +114 -0
- package/hooks/tool-ledger.py +165 -0
- package/hooks/trust_review.py +662 -0
- package/hooks/user-prompt-submit.py +12 -0
- package/hud/omg-hud.mjs +1571 -0
- package/lab/__init__.py +1 -0
- package/lab/__pycache__/__init__.cpython-313.pyc +0 -0
- package/lab/__pycache__/axolotl_adapter.cpython-313.pyc +0 -0
- package/lab/__pycache__/forge_runner.cpython-313.pyc +0 -0
- package/lab/__pycache__/gazebo_adapter.cpython-313.pyc +0 -0
- package/lab/__pycache__/isaac_gym_adapter.cpython-313.pyc +0 -0
- package/lab/__pycache__/mock_isaac_env.cpython-313.pyc +0 -0
- package/lab/__pycache__/pipeline.cpython-313.pyc +0 -0
- package/lab/__pycache__/policies.cpython-313.pyc +0 -0
- package/lab/__pycache__/pybullet_adapter.cpython-313.pyc +0 -0
- package/lab/axolotl_adapter.py +531 -0
- package/lab/forge_runner.py +103 -0
- package/lab/gazebo_adapter.py +168 -0
- package/lab/isaac_gym_adapter.py +190 -0
- package/lab/mock_isaac_env.py +47 -0
- package/lab/pipeline.py +712 -0
- package/lab/policies.py +52 -0
- package/lab/pybullet_adapter.py +192 -0
- package/package.json +61 -0
- package/plugins/README.md +78 -0
- package/plugins/__init__.py +1 -0
- package/plugins/__pycache__/__init__.cpython-313.pyc +0 -0
- package/plugins/advanced/commands/OMG-code-review.md +114 -0
- package/plugins/advanced/commands/OMG-deep-plan.md +266 -0
- package/plugins/advanced/commands/OMG-handoff.md +115 -0
- package/plugins/advanced/commands/OMG-learn.md +110 -0
- package/plugins/advanced/commands/OMG-maintainer.md +31 -0
- package/plugins/advanced/commands/OMG-ralph-start.md +43 -0
- package/plugins/advanced/commands/OMG-ralph-stop.md +23 -0
- package/plugins/advanced/commands/OMG-security-review.md +16 -0
- package/plugins/advanced/commands/OMG-sequential-thinking.md +20 -0
- package/plugins/advanced/commands/OMG-ship.md +46 -0
- package/plugins/advanced/commands/OMG:code-review.md +114 -0
- package/plugins/advanced/commands/OMG:deep-plan.md +266 -0
- package/plugins/advanced/commands/OMG:handoff.md +115 -0
- package/plugins/advanced/commands/OMG:learn.md +110 -0
- package/plugins/advanced/commands/OMG:maintainer.md +31 -0
- package/plugins/advanced/commands/OMG:ralph-start.md +43 -0
- package/plugins/advanced/commands/OMG:ralph-stop.md +23 -0
- package/plugins/advanced/commands/OMG:security-review.md +16 -0
- package/plugins/advanced/commands/OMG:sequential-thinking.md +20 -0
- package/plugins/advanced/commands/OMG:ship.md +46 -0
- package/plugins/advanced/plugin.json +104 -0
- package/plugins/core/plugin.json +204 -0
- package/plugins/dephealth/__init__.py +0 -0
- package/plugins/dephealth/__pycache__/__init__.cpython-313.pyc +0 -0
- package/plugins/dephealth/__pycache__/cve_scanner.cpython-313.pyc +0 -0
- package/plugins/dephealth/__pycache__/license_checker.cpython-313.pyc +0 -0
- package/plugins/dephealth/__pycache__/manifest_detector.cpython-313.pyc +0 -0
- package/plugins/dephealth/__pycache__/vuln_analyzer.cpython-313.pyc +0 -0
- package/plugins/dephealth/cve_scanner.py +279 -0
- package/plugins/dephealth/license_checker.py +135 -0
- package/plugins/dephealth/manifest_detector.py +423 -0
- package/plugins/dephealth/vuln_analyzer.py +176 -0
- package/plugins/testgen/__init__.py +0 -0
- package/plugins/testgen/__pycache__/__init__.cpython-313.pyc +0 -0
- package/plugins/testgen/__pycache__/codamosa_engine.cpython-313.pyc +0 -0
- package/plugins/testgen/__pycache__/edge_case_synthesizer.cpython-313.pyc +0 -0
- package/plugins/testgen/__pycache__/framework_detector.cpython-313.pyc +0 -0
- package/plugins/testgen/__pycache__/skeleton_generator.cpython-313.pyc +0 -0
- package/plugins/testgen/codamosa_engine.py +402 -0
- package/plugins/testgen/edge_case_synthesizer.py +184 -0
- package/plugins/testgen/framework_detector.py +271 -0
- package/plugins/testgen/skeleton_generator.py +219 -0
- package/plugins/viz/__init__.py +0 -0
- package/plugins/viz/__pycache__/__init__.cpython-313.pyc +0 -0
- package/plugins/viz/__pycache__/ast_parser.cpython-313.pyc +0 -0
- package/plugins/viz/__pycache__/diagram_generator.cpython-313.pyc +0 -0
- package/plugins/viz/__pycache__/graph_builder.cpython-313.pyc +0 -0
- package/plugins/viz/__pycache__/native_parsers.cpython-313.pyc +0 -0
- package/plugins/viz/__pycache__/regex_parser.cpython-313.pyc +0 -0
- package/plugins/viz/ast_parser.py +139 -0
- package/plugins/viz/diagram_generator.py +192 -0
- package/plugins/viz/graph_builder.py +444 -0
- package/plugins/viz/native_parsers.py +259 -0
- package/plugins/viz/regex_parser.py +112 -0
- package/pyproject.toml +143 -0
- package/registry/__init__.py +1 -0
- package/registry/__pycache__/__init__.cpython-313.pyc +0 -0
- package/registry/__pycache__/approval_artifact.cpython-313.pyc +0 -0
- package/registry/__pycache__/verify_artifact.cpython-313.pyc +0 -0
- package/registry/approval_artifact.py +236 -0
- package/registry/bundles/algorithms.yaml +45 -0
- package/registry/bundles/api-twin.yaml +48 -0
- package/registry/bundles/ast-pack.yaml +80 -0
- package/registry/bundles/claim-judge.yaml +49 -0
- package/registry/bundles/control-plane.yaml +192 -0
- package/registry/bundles/data-lineage.yaml +47 -0
- package/registry/bundles/delta-classifier.yaml +47 -0
- package/registry/bundles/eval-gate.yaml +47 -0
- package/registry/bundles/hash-edit.yaml +73 -0
- package/registry/bundles/health.yaml +45 -0
- package/registry/bundles/hook-governor.yaml +101 -0
- package/registry/bundles/incident-replay.yaml +47 -0
- package/registry/bundles/lsp-pack.yaml +80 -0
- package/registry/bundles/mcp-fabric.yaml +53 -0
- package/registry/bundles/plan-council.yaml +56 -0
- package/registry/bundles/preflight.yaml +48 -0
- package/registry/bundles/proof-gate.yaml +49 -0
- package/registry/bundles/remote-supervisor.yaml +49 -0
- package/registry/bundles/robotics.yaml +45 -0
- package/registry/bundles/secure-worktree-pipeline.yaml +69 -0
- package/registry/bundles/security-check.yaml +50 -0
- package/registry/bundles/terminal-lane.yaml +61 -0
- package/registry/bundles/test-intent-lock.yaml +49 -0
- package/registry/bundles/tracebank.yaml +47 -0
- package/registry/bundles/vision.yaml +45 -0
- package/registry/omg-capability.schema.json +378 -0
- package/registry/policy-packs/airgapped.lock.json +11 -0
- package/registry/policy-packs/airgapped.signature.json +10 -0
- package/registry/policy-packs/airgapped.yaml +16 -0
- package/registry/policy-packs/fintech.lock.json +11 -0
- package/registry/policy-packs/fintech.signature.json +10 -0
- package/registry/policy-packs/fintech.yaml +15 -0
- package/registry/policy-packs/locked-prod.lock.json +11 -0
- package/registry/policy-packs/locked-prod.signature.json +10 -0
- package/registry/policy-packs/locked-prod.yaml +18 -0
- package/registry/trusted_signers.json +44 -0
- package/registry/verify_artifact.py +493 -0
- package/runtime/__init__.py +36 -0
- package/runtime/__pycache__/__init__.cpython-313.pyc +0 -0
- package/runtime/__pycache__/adoption.cpython-313.pyc +0 -0
- package/runtime/__pycache__/agent_selector.cpython-313.pyc +0 -0
- package/runtime/__pycache__/api_twin.cpython-313.pyc +0 -0
- package/runtime/__pycache__/architecture_signal.cpython-313.pyc +0 -0
- package/runtime/__pycache__/artifact_parsers.cpython-313.pyc +0 -0
- package/runtime/__pycache__/asset_loader.cpython-313.pyc +0 -0
- package/runtime/__pycache__/background_verification.cpython-313.pyc +0 -0
- package/runtime/__pycache__/budget_envelopes.cpython-313.pyc +0 -0
- package/runtime/__pycache__/business_workflow.cpython-313.pyc +0 -0
- package/runtime/__pycache__/canonical_surface.cpython-313.pyc +0 -0
- package/runtime/__pycache__/canonical_taxonomy.cpython-313.pyc +0 -0
- package/runtime/__pycache__/claim_judge.cpython-313.pyc +0 -0
- package/runtime/__pycache__/cli_provider.cpython-313.pyc +0 -0
- package/runtime/__pycache__/compat.cpython-313.pyc +0 -0
- package/runtime/__pycache__/complexity_scorer.cpython-313.pyc +0 -0
- package/runtime/__pycache__/compliance_governor.cpython-313.pyc +0 -0
- package/runtime/__pycache__/config_transaction.cpython-313.pyc +0 -0
- package/runtime/__pycache__/context_compiler.cpython-313.pyc +0 -0
- package/runtime/__pycache__/context_engine.cpython-313.pyc +0 -0
- package/runtime/__pycache__/context_limits.cpython-313.pyc +0 -0
- package/runtime/__pycache__/contract_compiler.cpython-313.pyc +0 -0
- package/runtime/__pycache__/custom_agent_loader.cpython-313.pyc +0 -0
- package/runtime/__pycache__/data_lineage.cpython-313.pyc +0 -0
- package/runtime/__pycache__/defense_state.cpython-313.pyc +0 -0
- package/runtime/__pycache__/delta_classifier.cpython-313.pyc +0 -0
- package/runtime/__pycache__/dispatcher.cpython-313.pyc +0 -0
- package/runtime/__pycache__/doc_generator.cpython-313.pyc +0 -0
- package/runtime/__pycache__/domain_packs.cpython-313.pyc +0 -0
- package/runtime/__pycache__/ecosystem.cpython-313.pyc +0 -0
- package/runtime/__pycache__/equalizer.cpython-313.pyc +0 -0
- package/runtime/__pycache__/eval_gate.cpython-313.pyc +0 -0
- package/runtime/__pycache__/evidence_narrator.cpython-313.pyc +0 -0
- package/runtime/__pycache__/evidence_query.cpython-313.pyc +0 -0
- package/runtime/__pycache__/evidence_registry.cpython-313.pyc +0 -0
- package/runtime/__pycache__/evidence_requirements.cpython-313.pyc +0 -0
- package/runtime/__pycache__/exec_kernel.cpython-313.pyc +0 -0
- package/runtime/__pycache__/explainer_formatter.cpython-313.pyc +0 -0
- package/runtime/__pycache__/feature_registry.cpython-313.pyc +0 -0
- package/runtime/__pycache__/forge_agents.cpython-313.pyc +0 -0
- package/runtime/__pycache__/forge_contracts.cpython-313.pyc +0 -0
- package/runtime/__pycache__/forge_domains.cpython-313.pyc +0 -0
- package/runtime/__pycache__/forge_run_id.cpython-313.pyc +0 -0
- package/runtime/__pycache__/github_integration.cpython-313.pyc +0 -0
- package/runtime/__pycache__/github_review_bot.cpython-313.pyc +0 -0
- package/runtime/__pycache__/github_review_contract.cpython-313.pyc +0 -0
- package/runtime/__pycache__/github_review_formatter.cpython-313.pyc +0 -0
- package/runtime/__pycache__/guide_assert.cpython-313.pyc +0 -0
- package/runtime/__pycache__/hook_governor.cpython-313.pyc +0 -0
- package/runtime/__pycache__/host_parity.cpython-313.pyc +0 -0
- package/runtime/__pycache__/incident_replay.cpython-313.pyc +0 -0
- package/runtime/__pycache__/install_planner.cpython-313.pyc +0 -0
- package/runtime/__pycache__/interaction_journal.cpython-313.pyc +0 -0
- package/runtime/__pycache__/issue_surface.cpython-313.pyc +0 -0
- package/runtime/__pycache__/legacy_compat.cpython-313.pyc +0 -0
- package/runtime/__pycache__/mcp_config_writers.cpython-313.pyc +0 -0
- package/runtime/__pycache__/mcp_lifecycle.cpython-313.pyc +0 -0
- package/runtime/__pycache__/mcp_memory_server.cpython-313.pyc +0 -0
- package/runtime/__pycache__/memory_store.cpython-313.pyc +0 -0
- package/runtime/__pycache__/merge_writer.cpython-313.pyc +0 -0
- package/runtime/__pycache__/music_omr_testbed.cpython-313.pyc +0 -0
- package/runtime/__pycache__/mutation_gate.cpython-313.pyc +0 -0
- package/runtime/__pycache__/omc_compat.cpython-313.pyc +0 -0
- package/runtime/__pycache__/omg_browser_cli.cpython-313.pyc +0 -0
- package/runtime/__pycache__/omg_mcp_server.cpython-313.pyc +0 -0
- package/runtime/__pycache__/opus_plan.cpython-313.pyc +0 -0
- package/runtime/__pycache__/playwright_adapter.cpython-313.pyc +0 -0
- package/runtime/__pycache__/playwright_pack.cpython-313.pyc +0 -0
- package/runtime/__pycache__/plugin_diagnostics.cpython-313.pyc +0 -0
- package/runtime/__pycache__/plugin_interop.cpython-313.pyc +0 -0
- package/runtime/__pycache__/policy_pack_loader.cpython-313.pyc +0 -0
- package/runtime/__pycache__/preflight.cpython-313.pyc +0 -0
- package/runtime/__pycache__/profile_io.cpython-313.pyc +0 -0
- package/runtime/__pycache__/prompt_compiler.cpython-313.pyc +0 -0
- package/runtime/__pycache__/proof_chain.cpython-313.pyc +0 -0
- package/runtime/__pycache__/proof_gate.cpython-313.pyc +0 -0
- package/runtime/__pycache__/provider_parity_eval.cpython-313.pyc +0 -0
- package/runtime/__pycache__/release_artifact_audit.cpython-313.pyc +0 -0
- package/runtime/__pycache__/release_run_coordinator.cpython-313.pyc +0 -0
- package/runtime/__pycache__/release_surface_compiler.cpython-313.pyc +0 -0
- package/runtime/__pycache__/release_surface_registry.cpython-313.pyc +0 -0
- package/runtime/__pycache__/release_surfaces.cpython-313.pyc +0 -0
- package/runtime/__pycache__/remote_supervisor.cpython-313.pyc +0 -0
- package/runtime/__pycache__/repro_pack.cpython-313.pyc +0 -0
- package/runtime/__pycache__/rollback_manifest.cpython-313.pyc +0 -0
- package/runtime/__pycache__/router_critics.cpython-313.pyc +0 -0
- package/runtime/__pycache__/router_executor.cpython-313.pyc +0 -0
- package/runtime/__pycache__/router_selector.cpython-313.pyc +0 -0
- package/runtime/__pycache__/runtime_contracts.cpython-313.pyc +0 -0
- package/runtime/__pycache__/runtime_profile.cpython-313.pyc +0 -0
- package/runtime/__pycache__/security_check.cpython-313.pyc +0 -0
- package/runtime/__pycache__/session_health.cpython-313.pyc +0 -0
- package/runtime/__pycache__/skill_evolution.cpython-313.pyc +0 -0
- package/runtime/__pycache__/skill_registry.cpython-313.pyc +0 -0
- package/runtime/__pycache__/subagent_dispatcher.cpython-313.pyc +0 -0
- package/runtime/__pycache__/subscription_tiers.cpython-313.pyc +0 -0
- package/runtime/__pycache__/team_router.cpython-313.pyc +0 -0
- package/runtime/__pycache__/test_intent_lock.cpython-313-pytest-9.0.2.pyc +0 -0
- package/runtime/__pycache__/test_intent_lock.cpython-313.pyc +0 -0
- package/runtime/__pycache__/tmux_session_manager.cpython-313.pyc +0 -0
- package/runtime/__pycache__/tool_fabric.cpython-313.pyc +0 -0
- package/runtime/__pycache__/tool_plan_gate.cpython-313.pyc +0 -0
- package/runtime/__pycache__/tool_relevance.cpython-313.pyc +0 -0
- package/runtime/__pycache__/tracebank.cpython-313.pyc +0 -0
- package/runtime/__pycache__/untrusted_content.cpython-313.pyc +0 -0
- package/runtime/__pycache__/validate.cpython-313.pyc +0 -0
- package/runtime/__pycache__/verdict_schema.cpython-313.pyc +0 -0
- package/runtime/__pycache__/verification_controller.cpython-313.pyc +0 -0
- package/runtime/__pycache__/verification_loop.cpython-313.pyc +0 -0
- package/runtime/__pycache__/vision_artifacts.cpython-313.pyc +0 -0
- package/runtime/__pycache__/vision_cache.cpython-313.pyc +0 -0
- package/runtime/__pycache__/vision_jobs.cpython-313.pyc +0 -0
- package/runtime/__pycache__/worker_watchdog.cpython-313.pyc +0 -0
- package/runtime/adapters/__init__.py +13 -0
- package/runtime/adapters/__pycache__/__init__.cpython-313.pyc +0 -0
- package/runtime/adapters/__pycache__/claude.cpython-313.pyc +0 -0
- package/runtime/adapters/__pycache__/gpt.cpython-313.pyc +0 -0
- package/runtime/adapters/__pycache__/local.cpython-313.pyc +0 -0
- package/runtime/adapters/claude.py +63 -0
- package/runtime/adapters/gpt.py +56 -0
- package/runtime/adapters/local.py +56 -0
- package/runtime/adoption.py +280 -0
- package/runtime/api_twin.py +450 -0
- package/runtime/architecture_signal.py +226 -0
- package/runtime/artifact_parsers.py +161 -0
- package/runtime/asset_loader.py +62 -0
- package/runtime/background_verification.py +178 -0
- package/runtime/budget_envelopes.py +398 -0
- package/runtime/business_workflow.py +234 -0
- package/runtime/canonical_surface.py +53 -0
- package/runtime/canonical_taxonomy.py +27 -0
- package/runtime/claim_judge.py +648 -0
- package/runtime/cli_provider.py +105 -0
- package/runtime/compat.py +2222 -0
- package/runtime/complexity_scorer.py +148 -0
- package/runtime/compliance_governor.py +505 -0
- package/runtime/config_transaction.py +304 -0
- package/runtime/context_compiler.py +131 -0
- package/runtime/context_engine.py +708 -0
- package/runtime/context_limits.py +363 -0
- package/runtime/contract_compiler.py +3664 -0
- package/runtime/custom_agent_loader.py +366 -0
- package/runtime/data_lineage.py +244 -0
- package/runtime/defense_state.py +261 -0
- package/runtime/delta_classifier.py +231 -0
- package/runtime/dispatcher.py +47 -0
- package/runtime/doc_generator.py +319 -0
- package/runtime/domain_packs.py +75 -0
- package/runtime/ecosystem.py +371 -0
- package/runtime/equalizer.py +268 -0
- package/runtime/eval_gate.py +96 -0
- package/runtime/evidence_narrator.py +147 -0
- package/runtime/evidence_query.py +303 -0
- package/runtime/evidence_registry.py +16 -0
- package/runtime/evidence_requirements.py +157 -0
- package/runtime/exec_kernel.py +267 -0
- package/runtime/explainer_formatter.py +82 -0
- package/runtime/feature_registry.py +109 -0
- package/runtime/forge_agents.py +915 -0
- package/runtime/forge_contracts.py +519 -0
- package/runtime/forge_domains.py +68 -0
- package/runtime/forge_run_id.py +86 -0
- package/runtime/guide_assert.py +135 -0
- package/runtime/hook_governor.py +156 -0
- package/runtime/host_parity.py +373 -0
- package/runtime/incident_replay.py +310 -0
- package/runtime/install_planner.py +617 -0
- package/runtime/interaction_journal.py +566 -0
- package/runtime/issue_surface.py +472 -0
- package/runtime/legacy_compat.py +7 -0
- package/runtime/mcp_config_writers.py +360 -0
- package/runtime/mcp_lifecycle.py +175 -0
- package/runtime/mcp_memory_server.py +220 -0
- package/runtime/memory_parsers/__init__.py +0 -0
- package/runtime/memory_parsers/__pycache__/__init__.cpython-313.pyc +0 -0
- package/runtime/memory_parsers/__pycache__/chatgpt_parser.cpython-313.pyc +0 -0
- package/runtime/memory_parsers/__pycache__/claude_import.cpython-313.pyc +0 -0
- package/runtime/memory_parsers/__pycache__/export.cpython-313.pyc +0 -0
- package/runtime/memory_parsers/__pycache__/gemini_import.cpython-313.pyc +0 -0
- package/runtime/memory_parsers/__pycache__/kimi_import.cpython-313.pyc +0 -0
- package/runtime/memory_parsers/chatgpt_parser.py +257 -0
- package/runtime/memory_parsers/claude_import.py +107 -0
- package/runtime/memory_parsers/export.py +97 -0
- package/runtime/memory_parsers/gemini_import.py +91 -0
- package/runtime/memory_parsers/kimi_import.py +91 -0
- package/runtime/memory_store.py +1182 -0
- package/runtime/merge_writer.py +445 -0
- package/runtime/music_omr_testbed.py +336 -0
- package/runtime/mutation_gate.py +320 -0
- package/runtime/omc_compat.py +7 -0
- package/runtime/omg_browser_cli.py +95 -0
- package/runtime/omg_compat_contract_snapshot.json +936 -0
- package/runtime/omg_contract_snapshot.json +936 -0
- package/runtime/omg_mcp_server.py +306 -0
- package/runtime/playwright_adapter.py +39 -0
- package/runtime/playwright_pack.py +253 -0
- package/runtime/plugin_diagnostics.py +308 -0
- package/runtime/plugin_interop.py +1060 -0
- package/runtime/policy_pack_loader.py +147 -0
- package/runtime/preflight.py +135 -0
- package/runtime/profile_io.py +328 -0
- package/runtime/proof_chain.py +472 -0
- package/runtime/proof_gate.py +442 -0
- package/runtime/provider_parity_eval.py +109 -0
- package/runtime/providers/__init__.py +0 -0
- package/runtime/providers/__pycache__/__init__.cpython-313.pyc +0 -0
- package/runtime/providers/__pycache__/codex_provider.cpython-313.pyc +0 -0
- package/runtime/providers/__pycache__/gemini_provider.cpython-313.pyc +0 -0
- package/runtime/providers/__pycache__/kimi_provider.cpython-313.pyc +0 -0
- package/runtime/providers/__pycache__/opencode_provider.cpython-313.pyc +0 -0
- package/runtime/providers/codex_provider.py +129 -0
- package/runtime/providers/gemini_provider.py +143 -0
- package/runtime/providers/kimi_provider.py +167 -0
- package/runtime/providers/opencode_provider.py +99 -0
- package/runtime/release_artifact_audit.py +556 -0
- package/runtime/release_run_coordinator.py +574 -0
- package/runtime/release_surface_compiler.py +643 -0
- package/runtime/release_surface_registry.py +283 -0
- package/runtime/release_surfaces.py +320 -0
- package/runtime/remote_supervisor.py +79 -0
- package/runtime/repro_pack.py +398 -0
- package/runtime/rollback_manifest.py +143 -0
- package/runtime/router_critics.py +229 -0
- package/runtime/router_executor.py +142 -0
- package/runtime/router_selector.py +99 -0
- package/runtime/runtime_contracts.py +292 -0
- package/runtime/runtime_profile.py +133 -0
- package/runtime/security_check.py +1094 -0
- package/runtime/session_health.py +546 -0
- package/runtime/skill_evolution.py +221 -0
- package/runtime/skill_registry.py +53 -0
- package/runtime/subagent_dispatcher.py +604 -0
- package/runtime/subscription_tiers.py +258 -0
- package/runtime/team_router.py +1399 -0
- package/runtime/test_intent_lock.py +543 -0
- package/runtime/tmux_session_manager.py +172 -0
- package/runtime/tool_fabric.py +570 -0
- package/runtime/tool_plan_gate.py +460 -0
- package/runtime/tracebank.py +125 -0
- package/runtime/untrusted_content.py +360 -0
- package/runtime/validate.py +293 -0
- package/runtime/verdict_schema.py +198 -0
- package/runtime/verification_controller.py +235 -0
- package/runtime/verification_loop.py +73 -0
- package/runtime/vision_artifacts.py +31 -0
- package/runtime/vision_cache.py +38 -0
- package/runtime/vision_jobs.py +92 -0
- package/runtime/worker_watchdog.py +526 -0
- package/scripts/__pycache__/audit-published-artifact.cpython-313.pyc +0 -0
- package/scripts/__pycache__/check-doc-parity.cpython-313.pyc +0 -0
- package/scripts/__pycache__/check-omg-standalone-clean.cpython-313.pyc +0 -0
- package/scripts/__pycache__/github_review_helpers.cpython-313.pyc +0 -0
- package/scripts/__pycache__/omg.cpython-313.pyc +0 -0
- package/scripts/__pycache__/prepare-release-proof-fixtures.cpython-313.pyc +0 -0
- package/scripts/__pycache__/sync-release-identity.cpython-313.pyc +0 -0
- package/scripts/__pycache__/validate-release-identity.cpython-313.pyc +0 -0
- package/scripts/audit-published-artifact.py +59 -0
- package/scripts/check-omg-compat-contract-snapshot.py +137 -0
- package/scripts/check-omg-contract-snapshot.py +12 -0
- package/scripts/check-omg-public-ready.py +273 -0
- package/scripts/check-omg-standalone-clean.py +133 -0
- package/scripts/emit_host_parity.py +72 -0
- package/scripts/legacy_to_omg_migrate.py +29 -0
- package/scripts/migrate-legacy.py +464 -0
- package/scripts/omc_to_omg_migrate.py +12 -0
- package/scripts/omg.py +2962 -0
- package/scripts/pre-release-check.sh +38 -0
- package/scripts/prepare-release-proof-fixtures.py +602 -0
- package/scripts/print-canonical-version.py +80 -0
- package/scripts/settings-merge.py +289 -0
- package/scripts/sync-release-identity.py +481 -0
- package/scripts/validate-release-identity.py +632 -0
- package/scripts/verify-no-omc.sh +5 -0
- package/scripts/verify-standalone.sh +35 -0
- package/settings.json +751 -0
- package/tools/__init__.py +2 -0
- package/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- package/tools/__pycache__/browser_consent.cpython-313.pyc +0 -0
- package/tools/__pycache__/browser_stealth.cpython-313.pyc +0 -0
- package/tools/__pycache__/browser_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/changelog_generator.cpython-313.pyc +0 -0
- package/tools/__pycache__/commit_splitter.cpython-313.pyc +0 -0
- package/tools/__pycache__/config_discovery.cpython-313.pyc +0 -0
- package/tools/__pycache__/config_merger.cpython-313.pyc +0 -0
- package/tools/__pycache__/dashboard_generator.cpython-313.pyc +0 -0
- package/tools/__pycache__/git_inspector.cpython-313.pyc +0 -0
- package/tools/__pycache__/lsp_client.cpython-313.pyc +0 -0
- package/tools/__pycache__/lsp_operations.cpython-313.pyc +0 -0
- package/tools/__pycache__/pr_generator.cpython-313.pyc +0 -0
- package/tools/__pycache__/python_repl.cpython-313.pyc +0 -0
- package/tools/__pycache__/python_sandbox.cpython-313.pyc +0 -0
- package/tools/__pycache__/session_snapshot.cpython-313.pyc +0 -0
- package/tools/__pycache__/ssh_manager.cpython-313.pyc +0 -0
- package/tools/__pycache__/theme_engine.cpython-313.pyc +0 -0
- package/tools/__pycache__/theme_selector.cpython-313.pyc +0 -0
- package/tools/__pycache__/web_search.cpython-313.pyc +0 -0
- package/tools/browser_consent.py +289 -0
- package/tools/browser_stealth.py +481 -0
- package/tools/browser_tool.py +448 -0
- package/tools/changelog_generator.py +347 -0
- package/tools/commit_splitter.py +749 -0
- package/tools/config_discovery.py +151 -0
- package/tools/config_merger.py +449 -0
- package/tools/dashboard_generator.py +300 -0
- package/tools/git_inspector.py +298 -0
- package/tools/lsp_client.py +275 -0
- package/tools/lsp_discovery.py +231 -0
- package/tools/lsp_operations.py +392 -0
- package/tools/pr_generator.py +404 -0
- package/tools/python_repl.py +712 -0
- package/tools/python_sandbox.py +768 -0
- package/tools/search_providers/__init__.py +77 -0
- package/tools/search_providers/__pycache__/__init__.cpython-313.pyc +0 -0
- package/tools/search_providers/__pycache__/brave.cpython-313.pyc +0 -0
- package/tools/search_providers/__pycache__/exa.cpython-313.pyc +0 -0
- package/tools/search_providers/__pycache__/jina.cpython-313.pyc +0 -0
- package/tools/search_providers/__pycache__/perplexity.cpython-313.pyc +0 -0
- package/tools/search_providers/__pycache__/synthetic.cpython-313.pyc +0 -0
- package/tools/search_providers/brave.py +115 -0
- package/tools/search_providers/exa.py +116 -0
- package/tools/search_providers/jina.py +104 -0
- package/tools/search_providers/perplexity.py +139 -0
- package/tools/search_providers/synthetic.py +74 -0
- package/tools/session_snapshot.py +851 -0
- package/tools/ssh_manager.py +912 -0
- package/tools/theme_engine.py +296 -0
- package/tools/theme_selector.py +137 -0
- 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()
|