@trac3er/oh-my-god 2.1.0 → 2.1.4
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/.agents/skills/omg/AGENTS.fragment.md +2 -2
- package/.agents/skills/omg/algorithms/SKILL.md +1 -1
- package/.agents/skills/omg/algorithms/openai.yaml +1 -1
- package/.agents/skills/omg/api-twin/SKILL.md +1 -1
- package/.agents/skills/omg/api-twin/openai.yaml +1 -1
- package/.agents/skills/omg/claim-judge/SKILL.md +1 -1
- package/.agents/skills/omg/claim-judge/openai.yaml +1 -1
- package/.agents/skills/omg/codex-rules.md +1 -1
- package/.agents/skills/omg/control-plane/SKILL.md +1 -1
- package/.agents/skills/omg/control-plane/openai.yaml +1 -1
- package/.agents/skills/omg/data-lineage/SKILL.md +1 -1
- package/.agents/skills/omg/data-lineage/openai.yaml +1 -1
- package/.agents/skills/omg/delta-classifier/SKILL.md +1 -1
- package/.agents/skills/omg/delta-classifier/openai.yaml +1 -1
- package/.agents/skills/omg/eval-gate/SKILL.md +1 -1
- package/.agents/skills/omg/eval-gate/openai.yaml +1 -1
- package/.agents/skills/omg/health/SKILL.md +1 -1
- package/.agents/skills/omg/health/openai.yaml +1 -1
- package/.agents/skills/omg/hook-governor/SKILL.md +1 -1
- package/.agents/skills/omg/hook-governor/openai.yaml +1 -1
- package/.agents/skills/omg/incident-replay/SKILL.md +1 -1
- package/.agents/skills/omg/incident-replay/openai.yaml +1 -1
- package/.agents/skills/omg/lsp-pack/SKILL.md +1 -1
- package/.agents/skills/omg/lsp-pack/openai.yaml +1 -1
- package/.agents/skills/omg/mcp-fabric/SKILL.md +1 -1
- package/.agents/skills/omg/mcp-fabric/openai.yaml +1 -1
- package/.agents/skills/omg/plan-council/SKILL.md +1 -1
- package/.agents/skills/omg/plan-council/openai.yaml +1 -1
- package/.agents/skills/omg/preflight/SKILL.md +1 -1
- package/.agents/skills/omg/preflight/openai.yaml +1 -1
- package/.agents/skills/omg/proof-gate/SKILL.md +1 -1
- package/.agents/skills/omg/proof-gate/openai.yaml +1 -1
- package/.agents/skills/omg/remote-supervisor/SKILL.md +1 -1
- package/.agents/skills/omg/remote-supervisor/openai.yaml +1 -1
- package/.agents/skills/omg/robotics/SKILL.md +1 -1
- package/.agents/skills/omg/robotics/openai.yaml +1 -1
- package/.agents/skills/omg/secure-worktree-pipeline/SKILL.md +1 -1
- package/.agents/skills/omg/secure-worktree-pipeline/openai.yaml +1 -1
- package/.agents/skills/omg/security-check/SKILL.md +1 -1
- package/.agents/skills/omg/security-check/openai.yaml +1 -1
- package/.agents/skills/omg/test-intent-lock/SKILL.md +1 -1
- package/.agents/skills/omg/test-intent-lock/openai.yaml +1 -1
- package/.agents/skills/omg/tracebank/SKILL.md +1 -1
- package/.agents/skills/omg/tracebank/openai.yaml +1 -1
- package/.agents/skills/omg/vision/SKILL.md +1 -1
- package/.agents/skills/omg/vision/openai.yaml +1 -1
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +16 -0
- package/CLI-ADAPTER-MAP.md +3 -3
- package/OMG-setup.sh +114 -1
- package/OMG_COMPAT_CONTRACT.md +1 -1
- package/README.md +2 -1
- package/artifacts/release/.claude-plugin/marketplace.json +3 -3
- package/artifacts/release/.claude-plugin/plugin.json +1 -1
- package/artifacts/release/OMG_COMPAT_CONTRACT.md +1 -1
- package/artifacts/release/dist/enterprise/bundle/.claude-plugin/marketplace.json +3 -3
- package/artifacts/release/dist/enterprise/bundle/.claude-plugin/plugin.json +1 -1
- package/artifacts/release/dist/enterprise/bundle/OMG_COMPAT_CONTRACT.md +1 -1
- package/artifacts/release/dist/enterprise/bundle/plugins/advanced/plugin.json +26 -9
- package/artifacts/release/dist/enterprise/bundle/registry/bundles/algorithms.yaml +1 -1
- package/artifacts/release/dist/enterprise/bundle/registry/bundles/api-twin.yaml +1 -1
- package/artifacts/release/dist/enterprise/bundle/registry/bundles/claim-judge.yaml +1 -1
- package/artifacts/release/dist/enterprise/bundle/registry/bundles/control-plane.yaml +1 -1
- package/artifacts/release/dist/enterprise/bundle/registry/bundles/data-lineage.yaml +1 -1
- package/artifacts/release/dist/enterprise/bundle/registry/bundles/delta-classifier.yaml +1 -1
- package/artifacts/release/dist/enterprise/bundle/registry/bundles/eval-gate.yaml +1 -1
- package/artifacts/release/dist/enterprise/bundle/registry/bundles/health.yaml +1 -1
- package/artifacts/release/dist/enterprise/bundle/registry/bundles/hook-governor.yaml +1 -1
- package/artifacts/release/dist/enterprise/bundle/registry/bundles/incident-replay.yaml +1 -1
- package/artifacts/release/dist/enterprise/bundle/registry/bundles/lsp-pack.yaml +1 -1
- package/artifacts/release/dist/enterprise/bundle/registry/bundles/mcp-fabric.yaml +1 -1
- package/artifacts/release/dist/enterprise/bundle/registry/bundles/plan-council.yaml +1 -1
- package/artifacts/release/dist/enterprise/bundle/registry/bundles/preflight.yaml +1 -1
- package/artifacts/release/dist/enterprise/bundle/registry/bundles/proof-gate.yaml +1 -1
- package/artifacts/release/dist/enterprise/bundle/registry/bundles/remote-supervisor.yaml +1 -1
- package/artifacts/release/dist/enterprise/bundle/registry/bundles/robotics.yaml +1 -1
- package/artifacts/release/dist/enterprise/bundle/registry/bundles/secure-worktree-pipeline.yaml +1 -1
- package/artifacts/release/dist/enterprise/bundle/registry/bundles/security-check.yaml +1 -1
- package/artifacts/release/dist/enterprise/bundle/registry/bundles/test-intent-lock.yaml +1 -1
- package/artifacts/release/dist/enterprise/bundle/registry/bundles/tracebank.yaml +1 -1
- package/artifacts/release/dist/enterprise/bundle/registry/bundles/vision.yaml +1 -1
- package/artifacts/release/dist/enterprise/bundle/registry/omg-capability.schema.json +1 -1
- package/artifacts/release/dist/enterprise/bundle/settings.json +3 -3
- package/artifacts/release/dist/enterprise/manifest.json +30 -40
- package/artifacts/release/dist/public/bundle/.claude-plugin/marketplace.json +3 -3
- package/artifacts/release/dist/public/bundle/.claude-plugin/plugin.json +1 -1
- package/artifacts/release/dist/public/bundle/OMG_COMPAT_CONTRACT.md +1 -1
- package/artifacts/release/dist/public/bundle/plugins/advanced/plugin.json +26 -9
- package/artifacts/release/dist/public/bundle/registry/bundles/algorithms.yaml +1 -1
- package/artifacts/release/dist/public/bundle/registry/bundles/api-twin.yaml +1 -1
- package/artifacts/release/dist/public/bundle/registry/bundles/claim-judge.yaml +1 -1
- package/artifacts/release/dist/public/bundle/registry/bundles/control-plane.yaml +1 -1
- package/artifacts/release/dist/public/bundle/registry/bundles/data-lineage.yaml +1 -1
- package/artifacts/release/dist/public/bundle/registry/bundles/delta-classifier.yaml +1 -1
- package/artifacts/release/dist/public/bundle/registry/bundles/eval-gate.yaml +1 -1
- package/artifacts/release/dist/public/bundle/registry/bundles/health.yaml +1 -1
- package/artifacts/release/dist/public/bundle/registry/bundles/hook-governor.yaml +1 -1
- package/artifacts/release/dist/public/bundle/registry/bundles/incident-replay.yaml +1 -1
- package/artifacts/release/dist/public/bundle/registry/bundles/lsp-pack.yaml +1 -1
- package/artifacts/release/dist/public/bundle/registry/bundles/mcp-fabric.yaml +1 -1
- package/artifacts/release/dist/public/bundle/registry/bundles/plan-council.yaml +1 -1
- package/artifacts/release/dist/public/bundle/registry/bundles/preflight.yaml +1 -1
- package/artifacts/release/dist/public/bundle/registry/bundles/proof-gate.yaml +1 -1
- package/artifacts/release/dist/public/bundle/registry/bundles/remote-supervisor.yaml +1 -1
- package/artifacts/release/dist/public/bundle/registry/bundles/robotics.yaml +1 -1
- package/artifacts/release/dist/public/bundle/registry/bundles/secure-worktree-pipeline.yaml +1 -1
- package/artifacts/release/dist/public/bundle/registry/bundles/security-check.yaml +1 -1
- package/artifacts/release/dist/public/bundle/registry/bundles/test-intent-lock.yaml +1 -1
- package/artifacts/release/dist/public/bundle/registry/bundles/tracebank.yaml +1 -1
- package/artifacts/release/dist/public/bundle/registry/bundles/vision.yaml +1 -1
- package/artifacts/release/dist/public/bundle/registry/omg-capability.schema.json +1 -1
- package/artifacts/release/dist/public/bundle/settings.json +3 -3
- package/artifacts/release/dist/public/manifest.json +30 -40
- package/artifacts/release/plugins/advanced/plugin.json +26 -9
- package/artifacts/release/registry/bundles/algorithms.yaml +1 -1
- package/artifacts/release/registry/bundles/api-twin.yaml +1 -1
- package/artifacts/release/registry/bundles/claim-judge.yaml +1 -1
- package/artifacts/release/registry/bundles/control-plane.yaml +1 -1
- package/artifacts/release/registry/bundles/data-lineage.yaml +1 -1
- package/artifacts/release/registry/bundles/delta-classifier.yaml +1 -1
- package/artifacts/release/registry/bundles/eval-gate.yaml +1 -1
- package/artifacts/release/registry/bundles/health.yaml +1 -1
- package/artifacts/release/registry/bundles/hook-governor.yaml +1 -1
- package/artifacts/release/registry/bundles/incident-replay.yaml +1 -1
- package/artifacts/release/registry/bundles/lsp-pack.yaml +1 -1
- package/artifacts/release/registry/bundles/mcp-fabric.yaml +1 -1
- package/artifacts/release/registry/bundles/plan-council.yaml +1 -1
- package/artifacts/release/registry/bundles/preflight.yaml +1 -1
- package/artifacts/release/registry/bundles/proof-gate.yaml +1 -1
- package/artifacts/release/registry/bundles/remote-supervisor.yaml +1 -1
- package/artifacts/release/registry/bundles/robotics.yaml +1 -1
- package/artifacts/release/registry/bundles/secure-worktree-pipeline.yaml +1 -1
- package/artifacts/release/registry/bundles/security-check.yaml +1 -1
- package/artifacts/release/registry/bundles/test-intent-lock.yaml +1 -1
- package/artifacts/release/registry/bundles/tracebank.yaml +1 -1
- package/artifacts/release/registry/bundles/vision.yaml +1 -1
- package/artifacts/release/registry/omg-capability.schema.json +1 -1
- package/artifacts/release/settings.json +3 -3
- package/build/lib/commands/OMG:profile-review.md +43 -0
- package/build/lib/commands/OMG:validate.md +59 -0
- package/build/lib/control_plane/service.py +40 -1
- package/build/lib/hooks/_agent_registry.py +2 -0
- package/build/lib/hooks/firewall.py +263 -4
- package/build/lib/hooks/intentgate-keyword-detector.py +15 -3
- package/build/lib/hooks/policy_engine.py +77 -0
- package/build/lib/hooks/prompt-enhancer.py +146 -0
- package/build/lib/hooks/session-end-capture.py +374 -6
- package/build/lib/hooks/session-start.py +13 -35
- package/build/lib/hooks/setup_wizard.py +333 -1
- package/build/lib/hooks/shadow_manager.py +111 -2
- package/build/lib/hooks/stop_dispatcher.py +108 -4
- package/build/lib/plugins/advanced/plugin.json +26 -9
- package/build/lib/plugins/core/plugin.json +44 -11
- package/build/lib/registry/bundles/algorithms.yaml +1 -1
- package/build/lib/registry/bundles/api-twin.yaml +1 -1
- package/build/lib/registry/bundles/claim-judge.yaml +1 -1
- package/build/lib/registry/bundles/control-plane.yaml +1 -1
- package/build/lib/registry/bundles/data-lineage.yaml +1 -1
- package/build/lib/registry/bundles/delta-classifier.yaml +1 -1
- package/build/lib/registry/bundles/eval-gate.yaml +1 -1
- package/build/lib/registry/bundles/health.yaml +1 -1
- package/build/lib/registry/bundles/hook-governor.yaml +1 -1
- package/build/lib/registry/bundles/incident-replay.yaml +1 -1
- package/build/lib/registry/bundles/lsp-pack.yaml +1 -1
- package/build/lib/registry/bundles/mcp-fabric.yaml +1 -1
- package/build/lib/registry/bundles/plan-council.yaml +1 -1
- package/build/lib/registry/bundles/preflight.yaml +1 -1
- package/build/lib/registry/bundles/proof-gate.yaml +1 -1
- package/build/lib/registry/bundles/remote-supervisor.yaml +1 -1
- package/build/lib/registry/bundles/robotics.yaml +1 -1
- package/build/lib/registry/bundles/secure-worktree-pipeline.yaml +1 -1
- package/build/lib/registry/bundles/security-check.yaml +1 -1
- package/build/lib/registry/bundles/test-intent-lock.yaml +1 -1
- package/build/lib/registry/bundles/tracebank.yaml +1 -1
- package/build/lib/registry/bundles/vision.yaml +1 -1
- package/build/lib/registry/omg-capability.schema.json +1 -1
- package/build/lib/runtime/adoption.py +1 -1
- package/build/lib/runtime/background_verification.py +62 -0
- package/build/lib/runtime/claim_judge.py +165 -5
- package/build/lib/runtime/compat.py +15 -1
- package/build/lib/runtime/context_engine.py +261 -4
- package/build/lib/runtime/contract_compiler.py +370 -93
- package/build/lib/runtime/defense_state.py +79 -8
- package/build/lib/runtime/delta_classifier.py +80 -2
- package/build/lib/runtime/evidence_query.py +56 -4
- package/build/lib/runtime/evidence_registry.py +16 -0
- package/build/lib/runtime/evidence_requirements.py +47 -0
- package/build/lib/runtime/forge_agents.py +179 -9
- package/build/lib/runtime/forge_contracts.py +208 -2
- package/build/lib/runtime/interaction_journal.py +318 -42
- package/build/lib/runtime/memory_store.py +134 -1
- package/build/lib/runtime/mutation_gate.py +57 -11
- package/build/lib/runtime/omg_compat_contract_snapshot.json +2 -2
- package/build/lib/runtime/omg_mcp_server.py +14 -4
- package/build/lib/runtime/preflight.py +14 -0
- package/build/lib/runtime/profile_io.py +294 -0
- package/build/lib/runtime/proof_gate.py +122 -7
- package/build/lib/runtime/release_run_coordinator.py +339 -0
- package/build/lib/runtime/release_surfaces.py +243 -0
- package/build/lib/runtime/rollback_manifest.py +136 -0
- package/build/lib/runtime/router_critics.py +50 -0
- package/build/lib/runtime/runtime_contracts.py +137 -0
- package/build/lib/runtime/security_check.py +16 -0
- package/build/lib/runtime/session_health.py +60 -16
- package/build/lib/runtime/team_router.py +156 -13
- package/build/lib/runtime/test_intent_lock.py +102 -0
- package/build/lib/runtime/tool_plan_gate.py +249 -24
- package/build/lib/runtime/validate.py +194 -0
- package/build/lib/runtime/verification_controller.py +52 -2
- package/commands/OMG:diagnose-plugins.md +33 -0
- package/commands/OMG:profile-review.md +43 -0
- package/commands/OMG:setup.md +4 -1
- package/commands/OMG:validate.md +59 -0
- package/control_plane/service.py +40 -1
- package/dist/enterprise/bundle/.claude-plugin/marketplace.json +3 -3
- package/dist/enterprise/bundle/.claude-plugin/plugin.json +1 -1
- package/dist/enterprise/bundle/OMG_COMPAT_CONTRACT.md +1 -1
- package/dist/enterprise/bundle/plugins/advanced/plugin.json +26 -9
- package/dist/enterprise/bundle/registry/bundles/algorithms.yaml +1 -1
- package/dist/enterprise/bundle/registry/bundles/api-twin.yaml +1 -1
- package/dist/enterprise/bundle/registry/bundles/claim-judge.yaml +1 -1
- package/dist/enterprise/bundle/registry/bundles/control-plane.yaml +1 -1
- package/dist/enterprise/bundle/registry/bundles/data-lineage.yaml +1 -1
- package/dist/enterprise/bundle/registry/bundles/delta-classifier.yaml +1 -1
- package/dist/enterprise/bundle/registry/bundles/eval-gate.yaml +1 -1
- package/dist/enterprise/bundle/registry/bundles/health.yaml +1 -1
- package/dist/enterprise/bundle/registry/bundles/hook-governor.yaml +1 -1
- package/dist/enterprise/bundle/registry/bundles/incident-replay.yaml +1 -1
- package/dist/enterprise/bundle/registry/bundles/lsp-pack.yaml +1 -1
- package/dist/enterprise/bundle/registry/bundles/mcp-fabric.yaml +1 -1
- package/dist/enterprise/bundle/registry/bundles/plan-council.yaml +1 -1
- package/dist/enterprise/bundle/registry/bundles/preflight.yaml +1 -1
- package/dist/enterprise/bundle/registry/bundles/proof-gate.yaml +1 -1
- package/dist/enterprise/bundle/registry/bundles/remote-supervisor.yaml +1 -1
- package/dist/enterprise/bundle/registry/bundles/robotics.yaml +1 -1
- package/dist/enterprise/bundle/registry/bundles/secure-worktree-pipeline.yaml +1 -1
- package/dist/enterprise/bundle/registry/bundles/security-check.yaml +1 -1
- package/dist/enterprise/bundle/registry/bundles/test-intent-lock.yaml +1 -1
- package/dist/enterprise/bundle/registry/bundles/tracebank.yaml +1 -1
- package/dist/enterprise/bundle/registry/bundles/vision.yaml +1 -1
- package/dist/enterprise/bundle/registry/omg-capability.schema.json +1 -1
- package/dist/enterprise/bundle/settings.json +3 -3
- package/dist/enterprise/manifest.json +30 -40
- package/dist/oh_my_god-2.1.4-py3-none-any.whl +0 -0
- package/dist/oh_my_god-2.1.4.tar.gz +0 -0
- package/dist/public/bundle/.claude-plugin/marketplace.json +3 -3
- package/dist/public/bundle/.claude-plugin/plugin.json +1 -1
- package/dist/public/bundle/OMG_COMPAT_CONTRACT.md +1 -1
- package/dist/public/bundle/plugins/advanced/plugin.json +26 -9
- package/dist/public/bundle/registry/bundles/algorithms.yaml +1 -1
- package/dist/public/bundle/registry/bundles/api-twin.yaml +1 -1
- package/dist/public/bundle/registry/bundles/claim-judge.yaml +1 -1
- package/dist/public/bundle/registry/bundles/control-plane.yaml +1 -1
- package/dist/public/bundle/registry/bundles/data-lineage.yaml +1 -1
- package/dist/public/bundle/registry/bundles/delta-classifier.yaml +1 -1
- package/dist/public/bundle/registry/bundles/eval-gate.yaml +1 -1
- package/dist/public/bundle/registry/bundles/health.yaml +1 -1
- package/dist/public/bundle/registry/bundles/hook-governor.yaml +1 -1
- package/dist/public/bundle/registry/bundles/incident-replay.yaml +1 -1
- package/dist/public/bundle/registry/bundles/lsp-pack.yaml +1 -1
- package/dist/public/bundle/registry/bundles/mcp-fabric.yaml +1 -1
- package/dist/public/bundle/registry/bundles/plan-council.yaml +1 -1
- package/dist/public/bundle/registry/bundles/preflight.yaml +1 -1
- package/dist/public/bundle/registry/bundles/proof-gate.yaml +1 -1
- package/dist/public/bundle/registry/bundles/remote-supervisor.yaml +1 -1
- package/dist/public/bundle/registry/bundles/robotics.yaml +1 -1
- package/dist/public/bundle/registry/bundles/secure-worktree-pipeline.yaml +1 -1
- package/dist/public/bundle/registry/bundles/security-check.yaml +1 -1
- package/dist/public/bundle/registry/bundles/test-intent-lock.yaml +1 -1
- package/dist/public/bundle/registry/bundles/tracebank.yaml +1 -1
- package/dist/public/bundle/registry/bundles/vision.yaml +1 -1
- package/dist/public/bundle/registry/omg-capability.schema.json +1 -1
- package/dist/public/bundle/settings.json +3 -3
- package/dist/public/manifest.json +30 -40
- package/docs/install/opencode.md +30 -0
- package/docs/proof.md +11 -0
- package/docs/release-checklist.md +14 -1
- package/hooks/_agent_registry.py +2 -0
- package/hooks/firewall.py +263 -4
- package/hooks/intentgate-keyword-detector.py +15 -3
- package/hooks/policy_engine.py +77 -0
- package/hooks/prompt-enhancer.py +146 -0
- package/hooks/session-end-capture.py +374 -6
- package/hooks/session-start.py +13 -35
- package/hooks/setup_wizard.py +342 -2
- package/hooks/shadow_manager.py +111 -2
- package/hooks/stop_dispatcher.py +108 -4
- package/hud/omg-hud.mjs +30 -6
- package/lab/pipeline.py +197 -26
- package/package.json +1 -1
- package/plugins/advanced/plugin.json +26 -9
- package/plugins/core/plugin.json +51 -12
- package/pyproject.toml +10 -2
- package/registry/bundles/algorithms.yaml +1 -1
- package/registry/bundles/api-twin.yaml +1 -1
- package/registry/bundles/claim-judge.yaml +1 -1
- package/registry/bundles/control-plane.yaml +1 -1
- package/registry/bundles/data-lineage.yaml +1 -1
- package/registry/bundles/delta-classifier.yaml +1 -1
- package/registry/bundles/eval-gate.yaml +1 -1
- package/registry/bundles/health.yaml +1 -1
- package/registry/bundles/hook-governor.yaml +1 -1
- package/registry/bundles/incident-replay.yaml +1 -1
- package/registry/bundles/lsp-pack.yaml +1 -1
- package/registry/bundles/mcp-fabric.yaml +1 -1
- package/registry/bundles/plan-council.yaml +1 -1
- package/registry/bundles/preflight.yaml +1 -1
- package/registry/bundles/proof-gate.yaml +1 -1
- package/registry/bundles/remote-supervisor.yaml +1 -1
- package/registry/bundles/robotics.yaml +1 -1
- package/registry/bundles/secure-worktree-pipeline.yaml +1 -1
- package/registry/bundles/security-check.yaml +1 -1
- package/registry/bundles/test-intent-lock.yaml +1 -1
- package/registry/bundles/tracebank.yaml +1 -1
- package/registry/bundles/vision.yaml +1 -1
- package/registry/omg-capability.schema.json +1 -1
- package/runtime/adoption.py +7 -5
- package/runtime/background_verification.py +62 -0
- package/runtime/claim_judge.py +165 -5
- package/runtime/compat.py +48 -1
- package/runtime/context_engine.py +261 -4
- package/runtime/contract_compiler.py +370 -93
- package/runtime/defense_state.py +79 -8
- package/runtime/delta_classifier.py +80 -2
- package/runtime/evidence_query.py +56 -4
- package/runtime/evidence_registry.py +16 -0
- package/runtime/evidence_requirements.py +47 -0
- package/runtime/forge_agents.py +179 -9
- package/runtime/forge_contracts.py +208 -2
- package/runtime/hook_governor.py +19 -1
- package/runtime/interaction_journal.py +318 -42
- package/runtime/memory_store.py +134 -1
- package/runtime/mutation_gate.py +57 -11
- package/runtime/omg_compat_contract_snapshot.json +2 -2
- package/runtime/omg_mcp_server.py +14 -4
- package/runtime/plugin_diagnostics.py +248 -0
- package/runtime/plugin_interop.py +1007 -0
- package/runtime/preflight.py +14 -0
- package/runtime/profile_io.py +294 -0
- package/runtime/proof_gate.py +122 -7
- package/runtime/providers/opencode_provider.py +94 -0
- package/runtime/release_run_coordinator.py +339 -0
- package/runtime/release_surfaces.py +243 -0
- package/runtime/rollback_manifest.py +136 -0
- package/runtime/router_critics.py +50 -0
- package/runtime/runtime_contracts.py +137 -0
- package/runtime/security_check.py +16 -0
- package/runtime/session_health.py +60 -16
- package/runtime/team_router.py +156 -13
- package/runtime/test_intent_lock.py +102 -0
- package/runtime/tool_plan_gate.py +218 -19
- package/runtime/validate.py +289 -0
- package/runtime/verification_controller.py +52 -2
- package/scripts/omg.py +306 -3
- package/scripts/prepare-release-proof-fixtures.py +191 -7
- package/scripts/print-canonical-version.py +80 -0
- package/scripts/sync-release-identity.py +476 -0
- package/scripts/validate-release-identity.py +246 -0
- package/scripts/verify-standalone.sh +3 -0
- package/settings.json +3 -3
- package/templates/plugins-allowlist.yaml +19 -0
- package/artifacts/release/dist/enterprise/bundle/.gemini/settings.json +0 -11
- package/artifacts/release/dist/enterprise/bundle/.kimi/mcp.json +0 -11
- package/artifacts/release/dist/public/bundle/.gemini/settings.json +0 -11
- package/artifacts/release/dist/public/bundle/.kimi/mcp.json +0 -11
- package/build/lib/yaml.py +0 -321
- package/dist/enterprise/bundle/.gemini/settings.json +0 -11
- package/dist/enterprise/bundle/.kimi/mcp.json +0 -11
- package/dist/public/bundle/.gemini/settings.json +0 -11
- package/dist/public/bundle/.kimi/mcp.json +0 -11
|
@@ -0,0 +1,1007 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
import time
|
|
8
|
+
from collections.abc import Mapping
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import cast
|
|
13
|
+
import yaml
|
|
14
|
+
|
|
15
|
+
from runtime.hook_governor import validate_order
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Layer(str, Enum):
|
|
19
|
+
AUTHORED = "authored"
|
|
20
|
+
COMPILED = "compiled"
|
|
21
|
+
LIVE = "live"
|
|
22
|
+
DISCOVERED = "discovered"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Source(str, Enum):
|
|
26
|
+
PLUGIN_MANIFEST = "plugin_manifest"
|
|
27
|
+
MCP_JSON = "mcp_json"
|
|
28
|
+
COMPILED_BUNDLE = "compiled_bundle"
|
|
29
|
+
SKILL_REGISTRY = "skill_registry"
|
|
30
|
+
HOST_CONFIG = "host_config"
|
|
31
|
+
PLUGIN_DIR = "plugin_dir"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ConflictCode(str, Enum):
|
|
35
|
+
MCP_NAME_COLLISION = "mcp_name_collision"
|
|
36
|
+
COMMAND_COLLISION = "command_collision"
|
|
37
|
+
HOOK_ORDER_VIOLATION = "hook_order_violation"
|
|
38
|
+
PRESET_ESCALATION = "preset_escalation"
|
|
39
|
+
IDENTITY_DRIFT = "identity_drift"
|
|
40
|
+
UNSUPPORTED_HOST = "unsupported_host"
|
|
41
|
+
STALE_COMPILED_ARTIFACT = "stale_compiled_artifact"
|
|
42
|
+
PARTIAL_INSTALL = "partial_install"
|
|
43
|
+
HOST_PRECEDENCE_OVERLAP = "host_precedence_overlap"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ConflictSeverity(str, Enum):
|
|
47
|
+
BLOCKER = "blocker"
|
|
48
|
+
WARNING = "warning"
|
|
49
|
+
INFO = "info"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
CONFLICT_SEVERITY_MAP: dict[ConflictCode, ConflictSeverity] = {
|
|
53
|
+
ConflictCode.MCP_NAME_COLLISION: ConflictSeverity.BLOCKER,
|
|
54
|
+
ConflictCode.HOOK_ORDER_VIOLATION: ConflictSeverity.BLOCKER,
|
|
55
|
+
ConflictCode.COMMAND_COLLISION: ConflictSeverity.BLOCKER,
|
|
56
|
+
ConflictCode.PRESET_ESCALATION: ConflictSeverity.WARNING,
|
|
57
|
+
ConflictCode.IDENTITY_DRIFT: ConflictSeverity.WARNING,
|
|
58
|
+
ConflictCode.UNSUPPORTED_HOST: ConflictSeverity.WARNING,
|
|
59
|
+
ConflictCode.STALE_COMPILED_ARTIFACT: ConflictSeverity.INFO,
|
|
60
|
+
ConflictCode.PARTIAL_INSTALL: ConflictSeverity.INFO,
|
|
61
|
+
ConflictCode.HOST_PRECEDENCE_OVERLAP: ConflictSeverity.INFO,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
SUPPORTED_HOSTS: set[str] = {"claude", "codex", "gemini", "kimi", "opencode"}
|
|
65
|
+
SECURITY_PRETOOL_PLUGINS: tuple[str, ...] = ("firewall", "secret-guard")
|
|
66
|
+
PRESET_ORDER: tuple[str, ...] = ("safe", "balanced", "interop", "labs")
|
|
67
|
+
_PRESET_ORDER_INDEX: dict[str, int] = {name: idx for idx, name in enumerate(PRESET_ORDER)}
|
|
68
|
+
_SEVERITY_RANK: dict[str, int] = {
|
|
69
|
+
ConflictSeverity.BLOCKER.value: 0,
|
|
70
|
+
ConflictSeverity.WARNING.value: 1,
|
|
71
|
+
ConflictSeverity.INFO.value: 2,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(slots=True)
|
|
76
|
+
class PluginInteropRecord:
|
|
77
|
+
plugin_id: str
|
|
78
|
+
layer: str
|
|
79
|
+
host: str
|
|
80
|
+
source: str
|
|
81
|
+
commands: list[str] = field(default_factory=list)
|
|
82
|
+
hook_events: list[str] = field(default_factory=list)
|
|
83
|
+
mcp_servers: list[str] = field(default_factory=list)
|
|
84
|
+
preset_floor: str | None = None
|
|
85
|
+
enabled: bool = True
|
|
86
|
+
source_path: str | None = None
|
|
87
|
+
metadata: dict[str, object] = field(default_factory=dict)
|
|
88
|
+
|
|
89
|
+
def __post_init__(self) -> None:
|
|
90
|
+
valid_layers = {member.value for member in Layer}
|
|
91
|
+
valid_sources = {member.value for member in Source}
|
|
92
|
+
if self.layer not in valid_layers:
|
|
93
|
+
raise ValueError(f"Invalid layer: {self.layer}. Expected one of {sorted(valid_layers)}")
|
|
94
|
+
if self.source not in valid_sources:
|
|
95
|
+
raise ValueError(f"Invalid source: {self.source}. Expected one of {sorted(valid_sources)}")
|
|
96
|
+
|
|
97
|
+
def to_dict(self) -> dict[str, object]:
|
|
98
|
+
return {
|
|
99
|
+
"plugin_id": self.plugin_id,
|
|
100
|
+
"layer": self.layer,
|
|
101
|
+
"host": self.host,
|
|
102
|
+
"source": self.source,
|
|
103
|
+
"commands": list(self.commands),
|
|
104
|
+
"hook_events": list(self.hook_events),
|
|
105
|
+
"mcp_servers": list(self.mcp_servers),
|
|
106
|
+
"preset_floor": self.preset_floor,
|
|
107
|
+
"enabled": self.enabled,
|
|
108
|
+
"source_path": self.source_path,
|
|
109
|
+
"metadata": dict(self.metadata),
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def from_dict(cls, payload: Mapping[str, object]) -> PluginInteropRecord:
|
|
114
|
+
commands = _to_str_list(payload.get("commands"), "commands")
|
|
115
|
+
hook_events = _to_str_list(payload.get("hook_events"), "hook_events")
|
|
116
|
+
mcp_servers = _to_str_list(payload.get("mcp_servers"), "mcp_servers")
|
|
117
|
+
metadata = _to_str_object_dict(payload.get("metadata"), "metadata")
|
|
118
|
+
return cls(
|
|
119
|
+
plugin_id=_to_str_required(payload, "plugin_id"),
|
|
120
|
+
layer=_to_str_required(payload, "layer"),
|
|
121
|
+
host=_to_str_required(payload, "host"),
|
|
122
|
+
source=_to_str_required(payload, "source"),
|
|
123
|
+
commands=commands,
|
|
124
|
+
hook_events=hook_events,
|
|
125
|
+
mcp_servers=mcp_servers,
|
|
126
|
+
preset_floor=_to_optional_str(payload.get("preset_floor"), "preset_floor"),
|
|
127
|
+
enabled=_to_bool(payload.get("enabled", True), "enabled"),
|
|
128
|
+
source_path=_to_optional_str(payload.get("source_path"), "source_path"),
|
|
129
|
+
metadata=metadata,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass(slots=True)
|
|
134
|
+
class ConflictResult:
|
|
135
|
+
code: str
|
|
136
|
+
severity: str
|
|
137
|
+
affected_plugin_ids: list[str]
|
|
138
|
+
affected_hosts: list[str]
|
|
139
|
+
detail: str
|
|
140
|
+
next_action: str
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@dataclass(slots=True)
|
|
144
|
+
class PluginAllowlistEntry:
|
|
145
|
+
source: str
|
|
146
|
+
host: str
|
|
147
|
+
resource_type: str
|
|
148
|
+
reason: str
|
|
149
|
+
scope: str = "project"
|
|
150
|
+
timestamp: str | None = None
|
|
151
|
+
approver: str | None = None
|
|
152
|
+
|
|
153
|
+
def to_dict(self) -> dict[str, object]:
|
|
154
|
+
return {
|
|
155
|
+
"source": self.source,
|
|
156
|
+
"host": self.host,
|
|
157
|
+
"resource_type": self.resource_type,
|
|
158
|
+
"reason": self.reason,
|
|
159
|
+
"scope": self.scope,
|
|
160
|
+
"timestamp": self.timestamp,
|
|
161
|
+
"approver": self.approver,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def validate_plugin_allowlist_entry(payload: Mapping[str, object]) -> None:
|
|
166
|
+
source = payload.get("source")
|
|
167
|
+
host = payload.get("host")
|
|
168
|
+
resource_type = payload.get("resource_type")
|
|
169
|
+
reason = payload.get("reason")
|
|
170
|
+
scope = payload.get("scope", "project")
|
|
171
|
+
timestamp = payload.get("timestamp")
|
|
172
|
+
approver = payload.get("approver")
|
|
173
|
+
|
|
174
|
+
if not isinstance(source, str) or not source:
|
|
175
|
+
raise ValueError("source must be a non-empty string")
|
|
176
|
+
if not isinstance(host, str) or host not in SUPPORTED_HOSTS:
|
|
177
|
+
raise ValueError(f"host must be one of {sorted(SUPPORTED_HOSTS)}")
|
|
178
|
+
if not isinstance(resource_type, str) or resource_type not in {"mcp_server", "skill", "plugin"}:
|
|
179
|
+
raise ValueError("resource_type must be one of: mcp_server, skill, plugin")
|
|
180
|
+
if not isinstance(reason, str) or not reason.strip():
|
|
181
|
+
raise ValueError("reason must be a non-empty string")
|
|
182
|
+
if not isinstance(scope, str) or scope not in {"project", "user"}:
|
|
183
|
+
raise ValueError("scope must be 'project' or 'user'")
|
|
184
|
+
if timestamp is not None and not isinstance(timestamp, str):
|
|
185
|
+
raise ValueError("timestamp must be a string or None")
|
|
186
|
+
if approver is not None and not isinstance(approver, str):
|
|
187
|
+
raise ValueError("approver must be a string or None")
|
|
188
|
+
|
|
189
|
+
source_prefix, _, _ = source.partition(":")
|
|
190
|
+
expected_resource_by_prefix = {
|
|
191
|
+
"mcp": "mcp_server",
|
|
192
|
+
"skill": "skill",
|
|
193
|
+
"plugin": "plugin",
|
|
194
|
+
}
|
|
195
|
+
expected_resource = expected_resource_by_prefix.get(source_prefix)
|
|
196
|
+
if expected_resource is None:
|
|
197
|
+
raise ValueError("source must start with one of: mcp:, skill:, plugin:")
|
|
198
|
+
if resource_type != expected_resource:
|
|
199
|
+
raise ValueError(f"resource_type '{resource_type}' does not match source prefix '{source_prefix}:'")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def load_plugin_allowlist(root: str | None = None) -> list[PluginAllowlistEntry]:
|
|
203
|
+
root_path = Path(root or ".").resolve()
|
|
204
|
+
allowlist_path = root_path / ".omg" / "state" / "plugins-allowlist.yaml"
|
|
205
|
+
try:
|
|
206
|
+
raw = allowlist_path.read_text(encoding="utf-8")
|
|
207
|
+
except OSError:
|
|
208
|
+
return []
|
|
209
|
+
|
|
210
|
+
parsed_obj = cast(object, yaml.safe_load(raw))
|
|
211
|
+
if not isinstance(parsed_obj, list):
|
|
212
|
+
return []
|
|
213
|
+
|
|
214
|
+
entries: list[PluginAllowlistEntry] = []
|
|
215
|
+
for item in cast(list[object], parsed_obj):
|
|
216
|
+
if not isinstance(item, dict):
|
|
217
|
+
continue
|
|
218
|
+
payload = cast(dict[str, object], item)
|
|
219
|
+
try:
|
|
220
|
+
validate_plugin_allowlist_entry(payload)
|
|
221
|
+
except ValueError:
|
|
222
|
+
continue
|
|
223
|
+
entries.append(
|
|
224
|
+
PluginAllowlistEntry(
|
|
225
|
+
source=cast(str, payload["source"]),
|
|
226
|
+
host=cast(str, payload["host"]),
|
|
227
|
+
resource_type=cast(str, payload["resource_type"]),
|
|
228
|
+
reason=cast(str, payload["reason"]),
|
|
229
|
+
scope=cast(str, payload.get("scope", "project")),
|
|
230
|
+
timestamp=cast(str | None, payload.get("timestamp")),
|
|
231
|
+
approver=cast(str | None, payload.get("approver")),
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
return entries
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def save_plugin_allowlist(entries: list[PluginAllowlistEntry], root: str | None = None) -> str:
|
|
238
|
+
root_path = Path(root or ".").resolve()
|
|
239
|
+
state_dir = root_path / ".omg" / "state"
|
|
240
|
+
state_dir.mkdir(parents=True, exist_ok=True)
|
|
241
|
+
allowlist_path = state_dir / "plugins-allowlist.yaml"
|
|
242
|
+
|
|
243
|
+
serializable: list[dict[str, object]] = []
|
|
244
|
+
for entry in entries:
|
|
245
|
+
payload = entry.to_dict()
|
|
246
|
+
validate_plugin_allowlist_entry(payload)
|
|
247
|
+
serializable.append(payload)
|
|
248
|
+
|
|
249
|
+
dumped = yaml.safe_dump(serializable, sort_keys=False)
|
|
250
|
+
if not isinstance(dumped, str):
|
|
251
|
+
raise ValueError("plugins allowlist serialization failed")
|
|
252
|
+
_ = allowlist_path.write_text(cast(str, dumped), encoding="utf-8")
|
|
253
|
+
return str(allowlist_path)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@dataclass(slots=True)
|
|
257
|
+
class HookChainPlan:
|
|
258
|
+
event: str
|
|
259
|
+
chain: list[str]
|
|
260
|
+
status: str
|
|
261
|
+
blockers: list[str]
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def plan_hook_chain(event: str, foreign_hook_names: list[str]) -> HookChainPlan:
|
|
265
|
+
proposed_chain: list[str]
|
|
266
|
+
blockers: list[str] = []
|
|
267
|
+
|
|
268
|
+
if event == "PreToolUse":
|
|
269
|
+
for hook_name in foreign_hook_names:
|
|
270
|
+
if hook_name in SECURITY_PRETOOL_PLUGINS:
|
|
271
|
+
blockers.append(
|
|
272
|
+
f"foreign hook '{hook_name}' must not appear before immovable security hooks"
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
non_security_hooks = [name for name in foreign_hook_names if name not in SECURITY_PRETOOL_PLUGINS]
|
|
276
|
+
cleanup_hooks = [name for name in non_security_hooks if "cleanup" in name.lower()]
|
|
277
|
+
standard_hooks = [name for name in non_security_hooks if "cleanup" not in name.lower()]
|
|
278
|
+
proposed_chain = [*SECURITY_PRETOOL_PLUGINS, *standard_hooks, *cleanup_hooks]
|
|
279
|
+
else:
|
|
280
|
+
proposed_chain = list(foreign_hook_names)
|
|
281
|
+
|
|
282
|
+
validation = validate_order(event, proposed_chain)
|
|
283
|
+
validation_blockers = list(validation.get("blockers", []))
|
|
284
|
+
no_canonical_order = validation_blockers == [f"no canonical hook order for event {event}"]
|
|
285
|
+
if not (event != "PreToolUse" and no_canonical_order):
|
|
286
|
+
blockers.extend(validation_blockers)
|
|
287
|
+
|
|
288
|
+
status = "blocked" if blockers else "ok"
|
|
289
|
+
return HookChainPlan(event=event, chain=proposed_chain, status=status, blockers=blockers)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def classify_conflicts(records: list[PluginInteropRecord]) -> list[ConflictResult]:
|
|
293
|
+
if not records:
|
|
294
|
+
return []
|
|
295
|
+
|
|
296
|
+
conflicts: list[ConflictResult] = []
|
|
297
|
+
seen_signatures: set[tuple[str, tuple[str, ...], tuple[str, ...], str]] = set()
|
|
298
|
+
|
|
299
|
+
def add_conflict(
|
|
300
|
+
code: ConflictCode,
|
|
301
|
+
plugin_ids: list[str],
|
|
302
|
+
hosts: list[str],
|
|
303
|
+
detail: str,
|
|
304
|
+
next_action: str,
|
|
305
|
+
) -> None:
|
|
306
|
+
normalized_plugins = sorted(set(plugin_ids))
|
|
307
|
+
normalized_hosts = sorted(set(hosts))
|
|
308
|
+
signature = (code.value, tuple(normalized_plugins), tuple(normalized_hosts), detail)
|
|
309
|
+
if signature in seen_signatures:
|
|
310
|
+
return
|
|
311
|
+
seen_signatures.add(signature)
|
|
312
|
+
conflicts.append(
|
|
313
|
+
ConflictResult(
|
|
314
|
+
code=code.value,
|
|
315
|
+
severity=CONFLICT_SEVERITY_MAP[code].value,
|
|
316
|
+
affected_plugin_ids=normalized_plugins,
|
|
317
|
+
affected_hosts=normalized_hosts,
|
|
318
|
+
detail=detail,
|
|
319
|
+
next_action=next_action,
|
|
320
|
+
)
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
records_by_host_mcp: dict[tuple[str, str], list[PluginInteropRecord]] = {}
|
|
324
|
+
records_by_host_command: dict[tuple[str, str], list[PluginInteropRecord]] = {}
|
|
325
|
+
records_by_mcp_name: dict[str, list[PluginInteropRecord]] = {}
|
|
326
|
+
records_by_plugin_id: dict[str, list[PluginInteropRecord]] = {}
|
|
327
|
+
|
|
328
|
+
security_indices: dict[str, int] = {}
|
|
329
|
+
for idx, record in enumerate(records):
|
|
330
|
+
records_by_plugin_id.setdefault(record.plugin_id, []).append(record)
|
|
331
|
+
if "PreToolUse" in record.hook_events and record.plugin_id in SECURITY_PRETOOL_PLUGINS:
|
|
332
|
+
if record.plugin_id not in security_indices:
|
|
333
|
+
security_indices[record.plugin_id] = idx
|
|
334
|
+
|
|
335
|
+
for mcp_server in record.mcp_servers:
|
|
336
|
+
records_by_host_mcp.setdefault((record.host, mcp_server), []).append(record)
|
|
337
|
+
records_by_mcp_name.setdefault(mcp_server, []).append(record)
|
|
338
|
+
|
|
339
|
+
for command in record.commands:
|
|
340
|
+
records_by_host_command.setdefault((record.host, command), []).append(record)
|
|
341
|
+
|
|
342
|
+
for (host, mcp_server), owner_records in sorted(records_by_host_mcp.items()):
|
|
343
|
+
if len(owner_records) < 2:
|
|
344
|
+
continue
|
|
345
|
+
add_conflict(
|
|
346
|
+
ConflictCode.MCP_NAME_COLLISION,
|
|
347
|
+
[record.plugin_id for record in owner_records],
|
|
348
|
+
[host],
|
|
349
|
+
f"Multiple plugins on host '{host}' declare MCP server '{mcp_server}'.",
|
|
350
|
+
"Keep one owner per host/MCP name and rename or remove conflicting declarations.",
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
for (host, command), owner_records in sorted(records_by_host_command.items()):
|
|
354
|
+
if len(owner_records) < 2:
|
|
355
|
+
continue
|
|
356
|
+
add_conflict(
|
|
357
|
+
ConflictCode.COMMAND_COLLISION,
|
|
358
|
+
[record.plugin_id for record in owner_records],
|
|
359
|
+
[host],
|
|
360
|
+
f"Multiple plugins on host '{host}' declare command '{command}'.",
|
|
361
|
+
"Assign command ownership to one plugin per host or rename conflicting commands.",
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
firewall_index = security_indices.get("firewall")
|
|
365
|
+
secret_guard_index = security_indices.get("secret-guard")
|
|
366
|
+
for idx, record in enumerate(records):
|
|
367
|
+
if "PreToolUse" not in record.hook_events or record.plugin_id in SECURITY_PRETOOL_PLUGINS:
|
|
368
|
+
continue
|
|
369
|
+
before_firewall = firewall_index is not None and idx < firewall_index
|
|
370
|
+
before_secret_guard = secret_guard_index is not None and idx < secret_guard_index
|
|
371
|
+
if before_firewall or before_secret_guard:
|
|
372
|
+
add_conflict(
|
|
373
|
+
ConflictCode.HOOK_ORDER_VIOLATION,
|
|
374
|
+
[record.plugin_id] + list(SECURITY_PRETOOL_PLUGINS),
|
|
375
|
+
[record.host],
|
|
376
|
+
f"Plugin '{record.plugin_id}' registers PreToolUse before required security hooks.",
|
|
377
|
+
"Move firewall and secret-guard ahead of all non-security PreToolUse hooks.",
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
for record in records:
|
|
381
|
+
preset_floor = record.preset_floor
|
|
382
|
+
if preset_floor is None:
|
|
383
|
+
continue
|
|
384
|
+
preset_index = _PRESET_ORDER_INDEX.get(preset_floor)
|
|
385
|
+
safe_index = _PRESET_ORDER_INDEX["safe"]
|
|
386
|
+
if preset_index is not None and preset_index > safe_index:
|
|
387
|
+
add_conflict(
|
|
388
|
+
ConflictCode.PRESET_ESCALATION,
|
|
389
|
+
[record.plugin_id],
|
|
390
|
+
[record.host],
|
|
391
|
+
f"Plugin '{record.plugin_id}' raises preset floor to '{preset_floor}'.",
|
|
392
|
+
"Keep preset_floor at safe unless the higher floor is explicitly approved.",
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
for plugin_id, plugin_records in sorted(records_by_plugin_id.items()):
|
|
396
|
+
layers = sorted({record.layer for record in plugin_records})
|
|
397
|
+
if len(layers) > 1:
|
|
398
|
+
add_conflict(
|
|
399
|
+
ConflictCode.IDENTITY_DRIFT,
|
|
400
|
+
[plugin_id],
|
|
401
|
+
[record.host for record in plugin_records],
|
|
402
|
+
f"Plugin '{plugin_id}' appears with inconsistent layers: {', '.join(layers)}.",
|
|
403
|
+
"Align authored/compiled/live ownership metadata to a single layer identity.",
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
for record in records:
|
|
407
|
+
if record.host not in SUPPORTED_HOSTS:
|
|
408
|
+
add_conflict(
|
|
409
|
+
ConflictCode.UNSUPPORTED_HOST,
|
|
410
|
+
[record.plugin_id],
|
|
411
|
+
[record.host],
|
|
412
|
+
f"Plugin '{record.plugin_id}' targets unsupported host '{record.host}'.",
|
|
413
|
+
"Use one of the supported hosts: claude, codex, gemini, kimi, opencode.",
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
for record in records:
|
|
417
|
+
if record.layer == Layer.COMPILED.value and not record.enabled:
|
|
418
|
+
add_conflict(
|
|
419
|
+
ConflictCode.STALE_COMPILED_ARTIFACT,
|
|
420
|
+
[record.plugin_id],
|
|
421
|
+
[record.host],
|
|
422
|
+
f"Compiled artifact for plugin '{record.plugin_id}' is disabled.",
|
|
423
|
+
"Rebuild artifacts or remove stale compiled entries that are no longer active.",
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
for record in records:
|
|
427
|
+
if record.layer != Layer.AUTHORED.value:
|
|
428
|
+
continue
|
|
429
|
+
if record.commands or record.mcp_servers or record.hook_events:
|
|
430
|
+
continue
|
|
431
|
+
add_conflict(
|
|
432
|
+
ConflictCode.PARTIAL_INSTALL,
|
|
433
|
+
[record.plugin_id],
|
|
434
|
+
[record.host],
|
|
435
|
+
f"Authored plugin '{record.plugin_id}' has no commands, MCP servers, or hook events.",
|
|
436
|
+
"Finish installation wiring or remove placeholder authored plugin entries.",
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
for mcp_server, owner_records in sorted(records_by_mcp_name.items()):
|
|
440
|
+
hosts = sorted({record.host for record in owner_records})
|
|
441
|
+
if len(hosts) < 2:
|
|
442
|
+
continue
|
|
443
|
+
add_conflict(
|
|
444
|
+
ConflictCode.HOST_PRECEDENCE_OVERLAP,
|
|
445
|
+
[record.plugin_id for record in owner_records],
|
|
446
|
+
hosts,
|
|
447
|
+
f"MCP server '{mcp_server}' is declared across multiple hosts.",
|
|
448
|
+
"Validate intended host precedence and keep cross-host ownership explicit.",
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
return sorted(
|
|
452
|
+
conflicts,
|
|
453
|
+
key=lambda conflict: (
|
|
454
|
+
_SEVERITY_RANK.get(conflict.severity, 99),
|
|
455
|
+
conflict.code,
|
|
456
|
+
conflict.detail,
|
|
457
|
+
tuple(conflict.affected_plugin_ids),
|
|
458
|
+
tuple(conflict.affected_hosts),
|
|
459
|
+
),
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def approval_status_for_record(record: PluginInteropRecord, approvals: list[PluginAllowlistEntry]) -> str:
|
|
464
|
+
if record.host not in SUPPORTED_HOSTS:
|
|
465
|
+
return "blocked"
|
|
466
|
+
|
|
467
|
+
record_conflicts = classify_conflicts([record])
|
|
468
|
+
if any(conflict.severity == ConflictSeverity.BLOCKER.value for conflict in record_conflicts):
|
|
469
|
+
return "blocked"
|
|
470
|
+
|
|
471
|
+
approved_sources = _approval_sources_for_record(record)
|
|
472
|
+
for entry in approvals:
|
|
473
|
+
if entry.host == record.host and entry.source in approved_sources:
|
|
474
|
+
return "approved"
|
|
475
|
+
|
|
476
|
+
return "discoverable"
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def get_approval_status_for_all(
|
|
480
|
+
records: list[PluginInteropRecord], approvals: list[PluginAllowlistEntry]
|
|
481
|
+
) -> dict[str, str]:
|
|
482
|
+
return {record.plugin_id: approval_status_for_record(record, approvals) for record in records}
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _approval_sources_for_record(record: PluginInteropRecord) -> set[str]:
|
|
486
|
+
source_ids: set[str] = set()
|
|
487
|
+
for server_name in record.mcp_servers:
|
|
488
|
+
source_ids.add(f"mcp:{server_name}")
|
|
489
|
+
|
|
490
|
+
if record.source == Source.SKILL_REGISTRY.value:
|
|
491
|
+
source_ids.add(f"skill:{record.plugin_id}")
|
|
492
|
+
else:
|
|
493
|
+
source_ids.add(f"plugin:{record.plugin_id}")
|
|
494
|
+
|
|
495
|
+
return source_ids
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
@dataclass(slots=True)
|
|
499
|
+
class PluginInteropPayload:
|
|
500
|
+
records: list[PluginInteropRecord]
|
|
501
|
+
elapsed_ms: float
|
|
502
|
+
root: str
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
INTEROP_RECORD_SCHEMA: dict[str, object] = {
|
|
506
|
+
"type": "object",
|
|
507
|
+
"additionalProperties": False,
|
|
508
|
+
"required": ["plugin_id", "layer", "host", "source"],
|
|
509
|
+
"properties": {
|
|
510
|
+
"plugin_id": {"type": "string"},
|
|
511
|
+
"layer": {"type": "string", "enum": [member.value for member in Layer]},
|
|
512
|
+
"host": {"type": "string"},
|
|
513
|
+
"source": {"type": "string", "enum": [member.value for member in Source]},
|
|
514
|
+
"commands": {"type": "array", "items": {"type": "string"}, "default": []},
|
|
515
|
+
"hook_events": {"type": "array", "items": {"type": "string"}, "default": []},
|
|
516
|
+
"mcp_servers": {"type": "array", "items": {"type": "string"}, "default": []},
|
|
517
|
+
"preset_floor": {"type": ["string", "null"], "default": None},
|
|
518
|
+
"enabled": {"type": "boolean", "default": True},
|
|
519
|
+
"source_path": {"type": ["string", "null"], "default": None},
|
|
520
|
+
"metadata": {"type": "object", "default": {}},
|
|
521
|
+
},
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _to_str_required(payload: Mapping[str, object], key: str) -> str:
|
|
526
|
+
value = payload.get(key)
|
|
527
|
+
if not isinstance(value, str):
|
|
528
|
+
raise ValueError(f"{key} must be a string")
|
|
529
|
+
return value
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _to_optional_str(value: object, key: str) -> str | None:
|
|
533
|
+
if value is None:
|
|
534
|
+
return None
|
|
535
|
+
if isinstance(value, str):
|
|
536
|
+
return value
|
|
537
|
+
raise ValueError(f"{key} must be a string or None")
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _to_bool(value: object, key: str) -> bool:
|
|
541
|
+
if isinstance(value, bool):
|
|
542
|
+
return value
|
|
543
|
+
raise ValueError(f"{key} must be a boolean")
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def _to_str_list(value: object, key: str) -> list[str]:
|
|
547
|
+
if value is None:
|
|
548
|
+
return []
|
|
549
|
+
if not isinstance(value, list):
|
|
550
|
+
raise ValueError(f"{key} must be a list of strings")
|
|
551
|
+
result: list[str] = []
|
|
552
|
+
for item in cast(list[object], value):
|
|
553
|
+
if not isinstance(item, str):
|
|
554
|
+
raise ValueError(f"{key} must be a list of strings")
|
|
555
|
+
result.append(item)
|
|
556
|
+
return result
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def _to_str_object_dict(value: object, key: str) -> dict[str, object]:
|
|
560
|
+
if value is None:
|
|
561
|
+
return {}
|
|
562
|
+
if not isinstance(value, dict):
|
|
563
|
+
raise ValueError(f"{key} must be an object")
|
|
564
|
+
result: dict[str, object] = {}
|
|
565
|
+
for candidate_key, candidate_value in cast(dict[object, object], value).items():
|
|
566
|
+
if not isinstance(candidate_key, str):
|
|
567
|
+
raise ValueError(f"{key} keys must be strings")
|
|
568
|
+
result[candidate_key] = candidate_value
|
|
569
|
+
return result
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def discover_omg_plugin_state(root: str | None = None) -> PluginInteropPayload:
|
|
573
|
+
started = time.monotonic()
|
|
574
|
+
root_path = Path(root or ".").resolve()
|
|
575
|
+
records: list[PluginInteropRecord] = []
|
|
576
|
+
|
|
577
|
+
for manifest_path in root_path.glob("plugins/*/plugin.json"):
|
|
578
|
+
_append_plugin_manifest_record(records, manifest_path, layer=Layer.AUTHORED)
|
|
579
|
+
|
|
580
|
+
_append_plugin_manifest_record(records, root_path / ".claude-plugin" / "plugin.json", layer=Layer.AUTHORED)
|
|
581
|
+
|
|
582
|
+
for mcp_path in (root_path / ".claude-plugin" / "mcp.json", root_path / ".mcp.json"):
|
|
583
|
+
records.extend(_records_from_mcp_config(mcp_path))
|
|
584
|
+
|
|
585
|
+
records.extend(_records_from_skill_registry(root_path / ".omg" / "state" / "skill_registry" / "compact.json"))
|
|
586
|
+
|
|
587
|
+
build_lib_dir = root_path / "build" / "lib"
|
|
588
|
+
if build_lib_dir.is_dir():
|
|
589
|
+
for compiled_manifest_path in build_lib_dir.rglob("plugin.json"):
|
|
590
|
+
_append_plugin_manifest_record(records, compiled_manifest_path, layer=Layer.COMPILED)
|
|
591
|
+
|
|
592
|
+
elapsed_ms = (time.monotonic() - started) * 1000.0
|
|
593
|
+
return PluginInteropPayload(records=records, elapsed_ms=elapsed_ms, root=str(root_path))
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def discover_host_plugin_state(root: str | None = None) -> PluginInteropPayload:
|
|
597
|
+
started = time.monotonic()
|
|
598
|
+
root_path = Path(root or ".").resolve()
|
|
599
|
+
home_path = Path.home()
|
|
600
|
+
records: list[PluginInteropRecord] = []
|
|
601
|
+
|
|
602
|
+
records.extend(_records_from_codex_config(home_path / ".codex" / "config.toml"))
|
|
603
|
+
records.extend(_records_from_host_json_config(home_path / ".gemini" / "settings.json", host="gemini", mcp_key="mcpServers"))
|
|
604
|
+
records.extend(_records_from_host_json_config(home_path / ".kimi" / "mcp.json", host="kimi", mcp_key="mcpServers"))
|
|
605
|
+
records.extend(
|
|
606
|
+
_records_from_host_json_config(
|
|
607
|
+
home_path / ".config" / "opencode" / "opencode.json",
|
|
608
|
+
host="opencode",
|
|
609
|
+
mcp_key="mcp",
|
|
610
|
+
)
|
|
611
|
+
)
|
|
612
|
+
records.extend(_records_from_host_json_config(root_path / "opencode.json", host="opencode", mcp_key="mcp"))
|
|
613
|
+
records.extend(_records_from_opencode_plugin_dir(root_path / ".opencode" / "plugins"))
|
|
614
|
+
|
|
615
|
+
elapsed_ms = (time.monotonic() - started) * 1000.0
|
|
616
|
+
return PluginInteropPayload(records=records, elapsed_ms=elapsed_ms, root=str(root_path))
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def _append_plugin_manifest_record(
|
|
620
|
+
records: list[PluginInteropRecord],
|
|
621
|
+
manifest_path: Path,
|
|
622
|
+
*,
|
|
623
|
+
layer: Layer,
|
|
624
|
+
) -> None:
|
|
625
|
+
manifest = _read_json_dict(manifest_path)
|
|
626
|
+
if manifest is None:
|
|
627
|
+
return
|
|
628
|
+
|
|
629
|
+
commands = _extract_omg_commands(manifest)
|
|
630
|
+
plugin_name = manifest.get("name")
|
|
631
|
+
plugin_id = plugin_name if isinstance(plugin_name, str) and plugin_name else manifest_path.parent.name
|
|
632
|
+
records.append(
|
|
633
|
+
PluginInteropRecord(
|
|
634
|
+
plugin_id=plugin_id,
|
|
635
|
+
layer=layer.value,
|
|
636
|
+
host="claude",
|
|
637
|
+
source=Source.PLUGIN_MANIFEST.value if layer is Layer.AUTHORED else Source.COMPILED_BUNDLE.value,
|
|
638
|
+
commands=commands,
|
|
639
|
+
source_path=str(manifest_path),
|
|
640
|
+
)
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def _records_from_mcp_config(mcp_path: Path) -> list[PluginInteropRecord]:
|
|
645
|
+
mcp_config = _read_json_dict(mcp_path)
|
|
646
|
+
if mcp_config is None:
|
|
647
|
+
return []
|
|
648
|
+
|
|
649
|
+
mcp_servers = mcp_config.get("mcpServers")
|
|
650
|
+
if not isinstance(mcp_servers, dict):
|
|
651
|
+
return []
|
|
652
|
+
|
|
653
|
+
server_names = [name for name in cast(dict[object, object], mcp_servers).keys() if isinstance(name, str)]
|
|
654
|
+
records: list[PluginInteropRecord] = []
|
|
655
|
+
for server_name in server_names:
|
|
656
|
+
records.append(
|
|
657
|
+
PluginInteropRecord(
|
|
658
|
+
plugin_id=server_name,
|
|
659
|
+
layer=Layer.LIVE.value,
|
|
660
|
+
host="claude",
|
|
661
|
+
source=Source.MCP_JSON.value,
|
|
662
|
+
mcp_servers=[server_name],
|
|
663
|
+
source_path=str(mcp_path),
|
|
664
|
+
)
|
|
665
|
+
)
|
|
666
|
+
return records
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def _records_from_skill_registry(registry_path: Path) -> list[PluginInteropRecord]:
|
|
670
|
+
payload = _read_json_dict(registry_path)
|
|
671
|
+
if payload is None:
|
|
672
|
+
return []
|
|
673
|
+
|
|
674
|
+
skill_ids = _extract_skill_ids(payload)
|
|
675
|
+
return [
|
|
676
|
+
PluginInteropRecord(
|
|
677
|
+
plugin_id=skill_id,
|
|
678
|
+
layer=Layer.COMPILED.value,
|
|
679
|
+
host="claude",
|
|
680
|
+
source=Source.SKILL_REGISTRY.value,
|
|
681
|
+
source_path=str(registry_path),
|
|
682
|
+
)
|
|
683
|
+
for skill_id in skill_ids
|
|
684
|
+
]
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def _records_from_codex_config(config_path: Path) -> list[PluginInteropRecord]:
|
|
688
|
+
try:
|
|
689
|
+
lines = config_path.read_text(encoding="utf-8").splitlines()
|
|
690
|
+
except OSError:
|
|
691
|
+
return []
|
|
692
|
+
|
|
693
|
+
discovered: dict[str, bool] = {}
|
|
694
|
+
active_section: str | None = None
|
|
695
|
+
section_server: str | None = None
|
|
696
|
+
|
|
697
|
+
for raw_line in lines:
|
|
698
|
+
line = raw_line.strip()
|
|
699
|
+
if not line or line.startswith("#"):
|
|
700
|
+
continue
|
|
701
|
+
if "#" in line:
|
|
702
|
+
line = line.split("#", 1)[0].strip()
|
|
703
|
+
if not line:
|
|
704
|
+
continue
|
|
705
|
+
|
|
706
|
+
if line.startswith("[") and line.endswith("]"):
|
|
707
|
+
active_section = line[1:-1].strip()
|
|
708
|
+
section_server = _codex_server_from_section(active_section)
|
|
709
|
+
if section_server is not None:
|
|
710
|
+
_ = discovered.setdefault(section_server, True)
|
|
711
|
+
continue
|
|
712
|
+
|
|
713
|
+
if active_section == "mcp_servers" and "=" in line:
|
|
714
|
+
key, value = line.split("=", 1)
|
|
715
|
+
candidate = _unquote_toml_key(key.strip())
|
|
716
|
+
if candidate:
|
|
717
|
+
_ = discovered.setdefault(candidate, True)
|
|
718
|
+
inline_enabled = _extract_enabled_from_toml_value(value)
|
|
719
|
+
if inline_enabled is not None:
|
|
720
|
+
discovered[candidate] = inline_enabled
|
|
721
|
+
continue
|
|
722
|
+
|
|
723
|
+
if section_server is not None and "=" in line:
|
|
724
|
+
key, value = line.split("=", 1)
|
|
725
|
+
if key.strip() == "enabled":
|
|
726
|
+
enabled = _parse_toml_bool(value)
|
|
727
|
+
if enabled is not None:
|
|
728
|
+
discovered[section_server] = enabled
|
|
729
|
+
|
|
730
|
+
return [
|
|
731
|
+
PluginInteropRecord(
|
|
732
|
+
plugin_id=server_name,
|
|
733
|
+
layer=Layer.LIVE.value,
|
|
734
|
+
host="codex",
|
|
735
|
+
source=Source.HOST_CONFIG.value,
|
|
736
|
+
mcp_servers=[server_name],
|
|
737
|
+
enabled=enabled,
|
|
738
|
+
source_path=str(config_path),
|
|
739
|
+
)
|
|
740
|
+
for server_name, enabled in discovered.items()
|
|
741
|
+
]
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
def _records_from_host_json_config(config_path: Path, *, host: str, mcp_key: str) -> list[PluginInteropRecord]:
|
|
745
|
+
payload = _read_json_dict(config_path)
|
|
746
|
+
if payload is None:
|
|
747
|
+
return []
|
|
748
|
+
|
|
749
|
+
mcp_obj = payload.get(mcp_key)
|
|
750
|
+
if not isinstance(mcp_obj, dict):
|
|
751
|
+
return []
|
|
752
|
+
|
|
753
|
+
records: list[PluginInteropRecord] = []
|
|
754
|
+
for server_name, server_payload in cast(dict[object, object], mcp_obj).items():
|
|
755
|
+
if not isinstance(server_name, str):
|
|
756
|
+
continue
|
|
757
|
+
records.append(
|
|
758
|
+
PluginInteropRecord(
|
|
759
|
+
plugin_id=server_name,
|
|
760
|
+
layer=Layer.LIVE.value,
|
|
761
|
+
host=host,
|
|
762
|
+
source=Source.HOST_CONFIG.value,
|
|
763
|
+
mcp_servers=[server_name],
|
|
764
|
+
enabled=_extract_enabled_from_json_payload(server_payload),
|
|
765
|
+
source_path=str(config_path),
|
|
766
|
+
)
|
|
767
|
+
)
|
|
768
|
+
return records
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
def _records_from_opencode_plugin_dir(plugin_dir: Path) -> list[PluginInteropRecord]:
|
|
772
|
+
try:
|
|
773
|
+
entries = sorted(plugin_dir.iterdir(), key=lambda item: item.name)
|
|
774
|
+
except OSError:
|
|
775
|
+
return []
|
|
776
|
+
|
|
777
|
+
return [
|
|
778
|
+
PluginInteropRecord(
|
|
779
|
+
plugin_id=entry.name,
|
|
780
|
+
layer=Layer.DISCOVERED.value,
|
|
781
|
+
host="opencode",
|
|
782
|
+
source=Source.PLUGIN_DIR.value,
|
|
783
|
+
source_path=str(entry),
|
|
784
|
+
)
|
|
785
|
+
for entry in entries
|
|
786
|
+
]
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def _codex_server_from_section(section_name: str) -> str | None:
|
|
790
|
+
if not section_name.startswith("mcp_servers."):
|
|
791
|
+
return None
|
|
792
|
+
return _unquote_toml_key(section_name[len("mcp_servers.") :].strip())
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
def _unquote_toml_key(value: str) -> str:
|
|
796
|
+
if len(value) >= 2 and value[0] == '"' and value[-1] == '"':
|
|
797
|
+
return value[1:-1]
|
|
798
|
+
return value
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def _parse_toml_bool(value: str) -> bool | None:
|
|
802
|
+
normalized = value.strip().lower()
|
|
803
|
+
if normalized == "true":
|
|
804
|
+
return True
|
|
805
|
+
if normalized == "false":
|
|
806
|
+
return False
|
|
807
|
+
return None
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
def _extract_enabled_from_toml_value(value: str) -> bool | None:
|
|
811
|
+
match = re.search(r"\benabled\s*=\s*(true|false)\b", value, flags=re.IGNORECASE)
|
|
812
|
+
if match is None:
|
|
813
|
+
return None
|
|
814
|
+
return match.group(1).lower() == "true"
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
def _extract_enabled_from_json_payload(payload: object) -> bool:
|
|
818
|
+
if isinstance(payload, bool):
|
|
819
|
+
return payload
|
|
820
|
+
if isinstance(payload, dict):
|
|
821
|
+
enabled = cast(dict[object, object], payload).get("enabled")
|
|
822
|
+
if isinstance(enabled, bool):
|
|
823
|
+
return enabled
|
|
824
|
+
return True
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
def _extract_omg_commands(payload: Mapping[str, object]) -> list[str]:
|
|
828
|
+
commands = payload.get("commands")
|
|
829
|
+
if not isinstance(commands, dict):
|
|
830
|
+
return []
|
|
831
|
+
return [
|
|
832
|
+
key
|
|
833
|
+
for key in cast(dict[object, object], commands).keys()
|
|
834
|
+
if isinstance(key, str) and key.startswith("/OMG:")
|
|
835
|
+
]
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def _extract_skill_ids(payload: Mapping[str, object]) -> list[str]:
|
|
839
|
+
discovered: list[str] = []
|
|
840
|
+
|
|
841
|
+
skills = payload.get("skills")
|
|
842
|
+
if isinstance(skills, list):
|
|
843
|
+
for entry in cast(list[object], skills):
|
|
844
|
+
if isinstance(entry, str):
|
|
845
|
+
discovered.append(entry)
|
|
846
|
+
elif isinstance(entry, dict):
|
|
847
|
+
candidate = cast(dict[object, object], entry).get("id")
|
|
848
|
+
if isinstance(candidate, str):
|
|
849
|
+
discovered.append(candidate)
|
|
850
|
+
elif isinstance(skills, dict):
|
|
851
|
+
discovered.extend(key for key in cast(dict[object, object], skills).keys() if isinstance(key, str))
|
|
852
|
+
|
|
853
|
+
discovered.extend(key for key in payload if key not in {"skills", "schema", "version"})
|
|
854
|
+
|
|
855
|
+
deduped: list[str] = []
|
|
856
|
+
seen: set[str] = set()
|
|
857
|
+
for skill_id in discovered:
|
|
858
|
+
if skill_id not in seen:
|
|
859
|
+
seen.add(skill_id)
|
|
860
|
+
deduped.append(skill_id)
|
|
861
|
+
return deduped
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
def _read_json_dict(path: Path) -> dict[str, object] | None:
|
|
865
|
+
try:
|
|
866
|
+
raw_text = path.read_text(encoding="utf-8")
|
|
867
|
+
except OSError:
|
|
868
|
+
return None
|
|
869
|
+
|
|
870
|
+
try:
|
|
871
|
+
payload_obj = cast(object, json.loads(raw_text))
|
|
872
|
+
except json.JSONDecodeError:
|
|
873
|
+
return None
|
|
874
|
+
|
|
875
|
+
if not isinstance(payload_obj, dict):
|
|
876
|
+
return None
|
|
877
|
+
return cast(dict[str, object], payload_obj)
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
_SAFE_ENV_KEYS: frozenset[str] = frozenset({"PATH", "HOME", "TMPDIR"})
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
def probe_mcp_server_live(
|
|
884
|
+
server_name: str,
|
|
885
|
+
command: list[str],
|
|
886
|
+
timeout_ms: int = 1500,
|
|
887
|
+
) -> dict[str, object]:
|
|
888
|
+
"""Spawn an MCP server subprocess and send a JSON-RPC initialize request.
|
|
889
|
+
|
|
890
|
+
Returns a dict with ``server``, ``status`` (``ok`` | ``timeout`` | ``error``),
|
|
891
|
+
and ``elapsed_ms``. Only ``PATH``, ``HOME``, and ``TMPDIR`` are forwarded
|
|
892
|
+
from the current environment — no secrets leak to the child process.
|
|
893
|
+
"""
|
|
894
|
+
sanitized_env: dict[str, str] = {
|
|
895
|
+
key: os.environ[key] for key in _SAFE_ENV_KEYS if key in os.environ
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
initialize_request = json.dumps(
|
|
899
|
+
{
|
|
900
|
+
"jsonrpc": "2.0",
|
|
901
|
+
"id": 1,
|
|
902
|
+
"method": "initialize",
|
|
903
|
+
"params": {
|
|
904
|
+
"protocolVersion": "2024-11-05",
|
|
905
|
+
"capabilities": {},
|
|
906
|
+
"clientInfo": {"name": "omg-probe", "version": "0.1.0"},
|
|
907
|
+
},
|
|
908
|
+
}
|
|
909
|
+
) + "\n"
|
|
910
|
+
|
|
911
|
+
started = time.monotonic()
|
|
912
|
+
try:
|
|
913
|
+
process = subprocess.Popen(
|
|
914
|
+
command,
|
|
915
|
+
stdin=subprocess.PIPE,
|
|
916
|
+
stdout=subprocess.PIPE,
|
|
917
|
+
stderr=subprocess.PIPE,
|
|
918
|
+
env=sanitized_env,
|
|
919
|
+
)
|
|
920
|
+
except Exception as exc:
|
|
921
|
+
elapsed = (time.monotonic() - started) * 1000.0
|
|
922
|
+
return {
|
|
923
|
+
"server": server_name,
|
|
924
|
+
"status": "error",
|
|
925
|
+
"error": str(exc),
|
|
926
|
+
"elapsed_ms": elapsed,
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
try:
|
|
930
|
+
stdout_bytes, _ = process.communicate(
|
|
931
|
+
input=initialize_request.encode("utf-8"),
|
|
932
|
+
timeout=timeout_ms / 1000.0,
|
|
933
|
+
)
|
|
934
|
+
except subprocess.TimeoutExpired:
|
|
935
|
+
process.kill()
|
|
936
|
+
try:
|
|
937
|
+
process.communicate(timeout=2)
|
|
938
|
+
except Exception:
|
|
939
|
+
pass
|
|
940
|
+
elapsed = (time.monotonic() - started) * 1000.0
|
|
941
|
+
return {
|
|
942
|
+
"server": server_name,
|
|
943
|
+
"status": "timeout",
|
|
944
|
+
"elapsed_ms": elapsed,
|
|
945
|
+
}
|
|
946
|
+
except Exception as exc:
|
|
947
|
+
elapsed = (time.monotonic() - started) * 1000.0
|
|
948
|
+
try:
|
|
949
|
+
process.kill()
|
|
950
|
+
process.communicate(timeout=2)
|
|
951
|
+
except Exception:
|
|
952
|
+
pass
|
|
953
|
+
return {
|
|
954
|
+
"server": server_name,
|
|
955
|
+
"status": "error",
|
|
956
|
+
"error": str(exc),
|
|
957
|
+
"elapsed_ms": elapsed,
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
elapsed = (time.monotonic() - started) * 1000.0
|
|
961
|
+
|
|
962
|
+
tools: list[str] = []
|
|
963
|
+
try:
|
|
964
|
+
response = json.loads(stdout_bytes.decode("utf-8", errors="replace").strip())
|
|
965
|
+
if isinstance(response, dict):
|
|
966
|
+
caps = response.get("result", {})
|
|
967
|
+
if isinstance(caps, dict):
|
|
968
|
+
tool_list = caps.get("capabilities", {}).get("tools", [])
|
|
969
|
+
if isinstance(tool_list, list):
|
|
970
|
+
tools = [
|
|
971
|
+
t.get("name", "") for t in tool_list
|
|
972
|
+
if isinstance(t, dict) and isinstance(t.get("name"), str)
|
|
973
|
+
]
|
|
974
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
975
|
+
pass
|
|
976
|
+
|
|
977
|
+
return {
|
|
978
|
+
"server": server_name,
|
|
979
|
+
"status": "ok",
|
|
980
|
+
"tools": tools,
|
|
981
|
+
"elapsed_ms": elapsed,
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
__all__ = [
|
|
986
|
+
"CONFLICT_SEVERITY_MAP",
|
|
987
|
+
"ConflictResult",
|
|
988
|
+
"PluginAllowlistEntry",
|
|
989
|
+
"INTEROP_RECORD_SCHEMA",
|
|
990
|
+
"ConflictCode",
|
|
991
|
+
"ConflictSeverity",
|
|
992
|
+
"HookChainPlan",
|
|
993
|
+
"Layer",
|
|
994
|
+
"PluginInteropPayload",
|
|
995
|
+
"PluginInteropRecord",
|
|
996
|
+
"Source",
|
|
997
|
+
"approval_status_for_record",
|
|
998
|
+
"classify_conflicts",
|
|
999
|
+
"discover_host_plugin_state",
|
|
1000
|
+
"discover_omg_plugin_state",
|
|
1001
|
+
"get_approval_status_for_all",
|
|
1002
|
+
"load_plugin_allowlist",
|
|
1003
|
+
"plan_hook_chain",
|
|
1004
|
+
"save_plugin_allowlist",
|
|
1005
|
+
"probe_mcp_server_live",
|
|
1006
|
+
"validate_plugin_allowlist_entry",
|
|
1007
|
+
]
|