higpertext-cli 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- config/adapters_config.json +450 -0
- config/antigravity_agent_template.json +31 -0
- config/app_config.json +174 -0
- config/context_engine.json +33 -0
- config/environments/model_defaults.json +5 -0
- config/governance/branching_strategy.json +36 -0
- config/governance/deployment_gates.json +30 -0
- config/governance/guidelines_contract.json +54 -0
- config/governance/quality_gates.json +39 -0
- config/governance/section_rules.json +22 -0
- config/governance/security_guardrails.json +52 -0
- config/hooks/README.md +35 -0
- config/hooks/custom/test_output_limiter.json +9 -0
- config/hooks/global/session_prompt.json +9 -0
- config/htx_config.json +24 -0
- config/profile_learner.json +18 -0
- config/profiles/base_agent.json +40 -0
- config/profiles/base_auditor.json +19 -0
- config/profiles/base_developer.json +19 -0
- config/profiles/base_operator.json +16 -0
- config/profiles/global.json +33 -0
- config/profiles/software_developer.json +23 -0
- config/router_content.json +137 -0
- config/semantic_graph.json +66 -0
- config/workflows/ado_release_flow.json +38 -0
- config/workflows/docs-update.json +33 -0
- config/workflows/governance-check.yaml +26 -0
- config/workflows/guidelines-sync.json +40 -0
- config/workflows/higpertext-build.json +73 -0
- config/workflows/higpertext-plan.json +38 -0
- config/workflows/higpertext-review.json +41 -0
- config/workflows/pr-quality-check.json +56 -0
- config/workflows/quality-remediation.json +57 -0
- higpertext/__init__.py +18 -0
- higpertext/adapters/__init__.py +27 -0
- higpertext/adapters/adapter_utils.py +604 -0
- higpertext/adapters/claude_adapter/__init__.py +0 -0
- higpertext/adapters/claude_adapter/claude_adapter.py +154 -0
- higpertext/adapters/copilot_adapter/__init__.py +0 -0
- higpertext/adapters/copilot_adapter/copilot_adapter.py +231 -0
- higpertext/adapters/gemini_adapter/__init__.py +0 -0
- higpertext/adapters/gemini_adapter/gemini_adapter.py +211 -0
- higpertext/adapters/llm_formatter.py +46 -0
- higpertext/adapters/open_code_adapter/__init__.py +0 -0
- higpertext/adapters/open_code_adapter/open_code_adapter.py +480 -0
- higpertext/capabilities/capabilities_runner.py +216 -0
- higpertext/capabilities/common/agent-builder.json +54 -0
- higpertext/capabilities/common/agent-sync.json +34 -0
- higpertext/capabilities/common/code-skeletonizer.json +35 -0
- higpertext/capabilities/common/commit-report.json +42 -0
- higpertext/capabilities/common/context-assembler.json +37 -0
- higpertext/capabilities/common/context-budget-report.json +15 -0
- higpertext/capabilities/common/dep-manager.json +43 -0
- higpertext/capabilities/common/docs-sync.json +14 -0
- higpertext/capabilities/common/doctor.json +18 -0
- higpertext/capabilities/common/efficiency-meter.json +31 -0
- higpertext/capabilities/common/env-catalog.json +13 -0
- higpertext/capabilities/common/env-clean.json +14 -0
- higpertext/capabilities/common/env-logs.json +16 -0
- higpertext/capabilities/common/env-runner.json +23 -0
- higpertext/capabilities/common/env-status.json +13 -0
- higpertext/capabilities/common/env-stop.json +14 -0
- higpertext/capabilities/common/env-template.json +14 -0
- higpertext/capabilities/common/error-context-locator.json +23 -0
- higpertext/capabilities/common/eval-agent.json +33 -0
- higpertext/capabilities/common/file-map.json +17 -0
- higpertext/capabilities/common/governance-exception.json +54 -0
- higpertext/capabilities/common/graph-query.json +59 -0
- higpertext/capabilities/common/graph-rebuild.json +31 -0
- higpertext/capabilities/common/graph-visualize.json +37 -0
- higpertext/capabilities/common/grep-search.json +176 -0
- higpertext/capabilities/common/higpertext-tester.json +25 -0
- higpertext/capabilities/common/hook-health.json +19 -0
- higpertext/capabilities/common/hook-sync-check.json +19 -0
- higpertext/capabilities/common/hooks-manager.json +55 -0
- higpertext/capabilities/common/knowledge-asker.json +27 -0
- higpertext/capabilities/common/list-rules.json +27 -0
- higpertext/capabilities/common/llm-invoke.json +59 -0
- higpertext/capabilities/common/load-rules.json +37 -0
- higpertext/capabilities/common/memory-manager.json +65 -0
- higpertext/capabilities/common/quality-scan.json +21 -0
- higpertext/capabilities/common/quality-updater.json +35 -0
- higpertext/capabilities/common/rag-index.json +17 -0
- higpertext/capabilities/common/report-viewer.json +24 -0
- higpertext/capabilities/common/roadmap-report.json +37 -0
- higpertext/capabilities/common/scripts/_env_cli.py +65 -0
- higpertext/capabilities/common/scripts/agent_builder.py +60 -0
- higpertext/capabilities/common/scripts/agent_sync.py +56 -0
- higpertext/capabilities/common/scripts/ask_higpertext.py +38 -0
- higpertext/capabilities/common/scripts/code_skeletonizer.py +225 -0
- higpertext/capabilities/common/scripts/commit_report.py +134 -0
- higpertext/capabilities/common/scripts/context_assembler.py +70 -0
- higpertext/capabilities/common/scripts/context_budget_report.py +53 -0
- higpertext/capabilities/common/scripts/dep_manager.py +81 -0
- higpertext/capabilities/common/scripts/docs_sync.py +981 -0
- higpertext/capabilities/common/scripts/doctor.py +144 -0
- higpertext/capabilities/common/scripts/efficiency_meter.py +83 -0
- higpertext/capabilities/common/scripts/env_catalog.py +47 -0
- higpertext/capabilities/common/scripts/env_clean.py +30 -0
- higpertext/capabilities/common/scripts/env_logs.py +32 -0
- higpertext/capabilities/common/scripts/env_runner.py +53 -0
- higpertext/capabilities/common/scripts/env_status.py +38 -0
- higpertext/capabilities/common/scripts/env_stop.py +30 -0
- higpertext/capabilities/common/scripts/env_template.py +73 -0
- higpertext/capabilities/common/scripts/error_context_locator.py +138 -0
- higpertext/capabilities/common/scripts/eval_agent.py +80 -0
- higpertext/capabilities/common/scripts/file_map.py +95 -0
- higpertext/capabilities/common/scripts/governance_exception.py +116 -0
- higpertext/capabilities/common/scripts/graph_query.py +104 -0
- higpertext/capabilities/common/scripts/graph_rebuild.py +107 -0
- higpertext/capabilities/common/scripts/graph_visualize.py +76 -0
- higpertext/capabilities/common/scripts/grep_search.py +648 -0
- higpertext/capabilities/common/scripts/higpertext_tester.py +102 -0
- higpertext/capabilities/common/scripts/hook_health.py +149 -0
- higpertext/capabilities/common/scripts/hook_sync_check.py +134 -0
- higpertext/capabilities/common/scripts/hooks_manager.py +171 -0
- higpertext/capabilities/common/scripts/list_rules.py +175 -0
- higpertext/capabilities/common/scripts/llm_invoke.py +135 -0
- higpertext/capabilities/common/scripts/load_rules.py +379 -0
- higpertext/capabilities/common/scripts/memory_manager.py +210 -0
- higpertext/capabilities/common/scripts/presentation_engine.py +63 -0
- higpertext/capabilities/common/scripts/quality_scan.py +132 -0
- higpertext/capabilities/common/scripts/rag_index.py +39 -0
- higpertext/capabilities/common/scripts/report_viewer.py +106 -0
- higpertext/capabilities/common/scripts/roadmap_report.py +73 -0
- higpertext/capabilities/common/scripts/search_router.py +111 -0
- higpertext/capabilities/common/scripts/semantic_diff.py +166 -0
- higpertext/capabilities/common/scripts/semantic_search.py +43 -0
- higpertext/capabilities/common/scripts/session_control.py +136 -0
- higpertext/capabilities/common/scripts/smart_read.py +232 -0
- higpertext/capabilities/common/scripts/subagent_executor.py +143 -0
- higpertext/capabilities/common/scripts/sync_agents.py +353 -0
- higpertext/capabilities/common/scripts/task_decomposer.py +78 -0
- higpertext/capabilities/common/scripts/telemetry_report.py +36 -0
- higpertext/capabilities/common/search-router.json +24 -0
- higpertext/capabilities/common/semantic-diff.json +40 -0
- higpertext/capabilities/common/semantic-search.json +19 -0
- higpertext/capabilities/common/session-clean.json +20 -0
- higpertext/capabilities/common/session-start.json +44 -0
- higpertext/capabilities/common/smart-read.json +28 -0
- higpertext/capabilities/common/subagent-executor.json +25 -0
- higpertext/capabilities/common/sync-agents.json +32 -0
- higpertext/capabilities/common/task-decomposer.json +37 -0
- higpertext/capabilities/common/telemetry-report.json +23 -0
- higpertext/capabilities/git/__init__.py +0 -0
- higpertext/capabilities/git/committer.json +61 -0
- higpertext/capabilities/git/diff.json +33 -0
- higpertext/capabilities/git/ls-files.json +44 -0
- higpertext/capabilities/git/rm.json +27 -0
- higpertext/capabilities/git/scripts/__init__.py +0 -0
- higpertext/capabilities/git/scripts/commit_changes.py +1077 -0
- higpertext/capabilities/git/scripts/git_diff.py +171 -0
- higpertext/capabilities/git/scripts/git_ls_files.py +376 -0
- higpertext/capabilities/git/scripts/git_rm.py +62 -0
- higpertext/capabilities/security/k8s-auditor.json +33 -0
- higpertext/capabilities/security/scripts/k8s_auditor.py +307 -0
- higpertext/capabilities/security/scripts/secret_scanner.py +235 -0
- higpertext/capabilities/security/secret-scanner.json +32 -0
- higpertext/hooks/__init__.py +28 -0
- higpertext/hooks/_compat.py +27 -0
- higpertext/hooks/hook_tasks/__init__.py +1 -0
- higpertext/hooks/hook_tasks/_rules/__init__.py +0 -0
- higpertext/hooks/hook_tasks/_rules/bash_rules.py +635 -0
- higpertext/hooks/hook_tasks/_rules/context_engine_rule.py +79 -0
- higpertext/hooks/hook_tasks/_rules/context_rules.py +199 -0
- higpertext/hooks/hook_tasks/_rules/governance_adapter.py +72 -0
- higpertext/hooks/hook_tasks/_rules/profile_rules.json +25 -0
- higpertext/hooks/hook_tasks/_rules/quality_rules.py +86 -0
- higpertext/hooks/hook_tasks/_rules/security_rules.py +214 -0
- higpertext/hooks/hook_tasks/_rules/session_rules.py +316 -0
- higpertext/hooks/hook_tasks/_rules/telemetry_rules.py +121 -0
- higpertext/hooks/hook_tasks/audit_logger_hook.py +28 -0
- higpertext/hooks/hook_tasks/hook_bash_guard.py +101 -0
- higpertext/hooks/hook_tasks/hook_code_quality.py +48 -0
- higpertext/hooks/hook_tasks/hook_context_hint.py +46 -0
- higpertext/hooks/hook_tasks/hook_context_manager.py +44 -0
- higpertext/hooks/hook_tasks/hook_io.py +122 -0
- higpertext/hooks/hook_tasks/hook_loop_guard.py +182 -0
- higpertext/hooks/hook_tasks/hook_post_observer.py +54 -0
- higpertext/hooks/hook_tasks/hook_read_guard.py +85 -0
- higpertext/hooks/hook_tasks/hook_security_guard.py +81 -0
- higpertext/hooks/hook_tasks/hook_session_prompt.py +83 -0
- higpertext/hooks/hook_tasks/hook_session_stop.py +115 -0
- higpertext/hooks/hook_tasks/hook_utils.py +144 -0
- higpertext/hooks/hook_tasks/session_guard_hook.py +23 -0
- higpertext/hooks/hook_tasks/telemetry_utils.py +176 -0
- higpertext/hooks/hook_tasks/test_echo_hook.py +33 -0
- higpertext/hooks/hook_tasks/webhook_hook.py +54 -0
- higpertext/hooks/hook_tasks/workflow_runner_hook.py +49 -0
- higpertext/hooks/hooks_catalog.json +116 -0
- higpertext/kernel/__init__.py +63 -0
- higpertext/kernel/_compat.py +138 -0
- higpertext/kernel/app_config.py +117 -0
- higpertext/kernel/application/__init__.py +13 -0
- higpertext/kernel/application/agent_registry.py +102 -0
- higpertext/kernel/application/capability_manager.py +61 -0
- higpertext/kernel/application/commit_reporter.py +247 -0
- higpertext/kernel/application/context_builder.py +166 -0
- higpertext/kernel/application/context_engine.py +409 -0
- higpertext/kernel/application/engine.py +41 -0
- higpertext/kernel/application/env_runtime.py +174 -0
- higpertext/kernel/application/environment_manager.py +154 -0
- higpertext/kernel/application/governance.py +192 -0
- higpertext/kernel/application/hook_registry.py +102 -0
- higpertext/kernel/application/hook_renderer.py +720 -0
- higpertext/kernel/application/ports.py +49 -0
- higpertext/kernel/application/profile_learner.py +358 -0
- higpertext/kernel/application/profile_service.py +205 -0
- higpertext/kernel/application/profile_services.py +6 -0
- higpertext/kernel/application/profile_use_cases.py +93 -0
- higpertext/kernel/application/rag_service.py +75 -0
- higpertext/kernel/application/roadmap_reporter.py +178 -0
- higpertext/kernel/application/semantic_engine.py +258 -0
- higpertext/kernel/application/session_services.py +33 -0
- higpertext/kernel/application/skill_hook_compiler.py +85 -0
- higpertext/kernel/application/telemetry.py +326 -0
- higpertext/kernel/application/workflow_manager.py +176 -0
- higpertext/kernel/config_paths.py +66 -0
- higpertext/kernel/domain/__init__.py +12 -0
- higpertext/kernel/domain/agent_registry.py +23 -0
- higpertext/kernel/domain/commit_reporter.py +155 -0
- higpertext/kernel/domain/compilers.py +7 -0
- higpertext/kernel/domain/context_engine.py +319 -0
- higpertext/kernel/domain/entities.py +51 -0
- higpertext/kernel/domain/env_runtime.py +62 -0
- higpertext/kernel/domain/governance.py +198 -0
- higpertext/kernel/domain/hook_models.py +29 -0
- higpertext/kernel/domain/profile_learner.py +186 -0
- higpertext/kernel/domain/rag.py +70 -0
- higpertext/kernel/domain/repositories.py +8 -0
- higpertext/kernel/domain/roadmap_reporter.py +80 -0
- higpertext/kernel/domain/semantic_engine.py +107 -0
- higpertext/kernel/engine.py +42 -0
- higpertext/kernel/htx_resolver.py +69 -0
- higpertext/kernel/infrastructure/__init__.py +13 -0
- higpertext/kernel/infrastructure/agent_registry.py +40 -0
- higpertext/kernel/infrastructure/cache/capability_cache.py +319 -0
- higpertext/kernel/infrastructure/capability_helper.py +40 -0
- higpertext/kernel/infrastructure/cli/__init__.py +1 -0
- higpertext/kernel/infrastructure/cli/agent_commands.py +62 -0
- higpertext/kernel/infrastructure/cli/arguments.py +39 -0
- higpertext/kernel/infrastructure/cli/capability_command_builder.py +86 -0
- higpertext/kernel/infrastructure/cli/capability_task_service.py +234 -0
- higpertext/kernel/infrastructure/cli/cli_search.py +234 -0
- higpertext/kernel/infrastructure/cli/parameter_contracts.py +83 -0
- higpertext/kernel/infrastructure/cli/parser_builder.py +122 -0
- higpertext/kernel/infrastructure/cli/profile_commands.py +89 -0
- higpertext/kernel/infrastructure/cli/roadmap_commands.py +117 -0
- higpertext/kernel/infrastructure/cli/router.py +1110 -0
- higpertext/kernel/infrastructure/cli/session_commands.py +36 -0
- higpertext/kernel/infrastructure/cli/task_commands.py +23 -0
- higpertext/kernel/infrastructure/cli/task_result_reporter.py +56 -0
- higpertext/kernel/infrastructure/cli/workflow_commands.py +25 -0
- higpertext/kernel/infrastructure/compilers/__init__.py +3 -0
- higpertext/kernel/infrastructure/compilers/factory.py +27 -0
- higpertext/kernel/infrastructure/compilers/graph_compiler.py +20 -0
- higpertext/kernel/infrastructure/compilers/guide_compiler.py +50 -0
- higpertext/kernel/infrastructure/compilers/hook_compiler.py +69 -0
- higpertext/kernel/infrastructure/compilers/playbook_compiler.py +154 -0
- higpertext/kernel/infrastructure/context_engine.py +303 -0
- higpertext/kernel/infrastructure/database/local_vector_store.py +99 -0
- higpertext/kernel/infrastructure/deployment/__init__.py +1 -0
- higpertext/kernel/infrastructure/deployment/resource_deployer.py +283 -0
- higpertext/kernel/infrastructure/diagnostics/__init__.py +1 -0
- higpertext/kernel/infrastructure/diagnostics/health.py +191 -0
- higpertext/kernel/infrastructure/env_runtime.py +227 -0
- higpertext/kernel/infrastructure/execution/__init__.py +1 -0
- higpertext/kernel/infrastructure/execution/parallel.py +188 -0
- higpertext/kernel/infrastructure/execution/resilience.py +155 -0
- higpertext/kernel/infrastructure/file_repositories.py +213 -0
- higpertext/kernel/infrastructure/governance.py +198 -0
- higpertext/kernel/infrastructure/hook_config_loader.py +53 -0
- higpertext/kernel/infrastructure/hook_webhook_dispatcher.py +61 -0
- higpertext/kernel/infrastructure/hook_workflow_bridge.py +60 -0
- higpertext/kernel/infrastructure/llm/__init__.py +6 -0
- higpertext/kernel/infrastructure/llm/provider.py +46 -0
- higpertext/kernel/infrastructure/llm/providers/__init__.py +0 -0
- higpertext/kernel/infrastructure/llm/providers/anthropic_provider.py +94 -0
- higpertext/kernel/infrastructure/llm/providers/gemini_embeddings.py +74 -0
- higpertext/kernel/infrastructure/llm/providers/gemini_provider.py +101 -0
- higpertext/kernel/infrastructure/llm/providers/ollama_provider.py +110 -0
- higpertext/kernel/infrastructure/llm/providers/openai_provider.py +98 -0
- higpertext/kernel/infrastructure/llm/registry.py +81 -0
- higpertext/kernel/infrastructure/logger.py +303 -0
- higpertext/kernel/infrastructure/output_store.py +70 -0
- higpertext/kernel/infrastructure/parser/__init__.py +1 -0
- higpertext/kernel/infrastructure/parser/code_chunker.py +144 -0
- higpertext/kernel/infrastructure/parser/language/__init__.py +14 -0
- higpertext/kernel/infrastructure/parser/language/base.py +41 -0
- higpertext/kernel/infrastructure/parser/language/powershell_parser.py +35 -0
- higpertext/kernel/infrastructure/parser/language/python_parser.py +98 -0
- higpertext/kernel/infrastructure/parser/language/typescript_parser.py +91 -0
- higpertext/kernel/infrastructure/parser/semantic_graph.py +409 -0
- higpertext/kernel/infrastructure/presentation/__init__.py +1 -0
- higpertext/kernel/infrastructure/presentation/html_renderer.py +137 -0
- higpertext/kernel/infrastructure/presentation/markdown_renderer.py +84 -0
- higpertext/kernel/infrastructure/presentation/markdown_report_renderer.py +97 -0
- higpertext/kernel/infrastructure/profile_store.py +28 -0
- higpertext/kernel/infrastructure/semantic_engine.py +289 -0
- higpertext/kernel/infrastructure/telemetry_reporter.py +132 -0
- higpertext/kernel/infrastructure/validation/__init__.py +1 -0
- higpertext/kernel/infrastructure/validation/contract_validator.py +163 -0
- higpertext/kernel/pkg_resources.py +38 -0
- higpertext/kernel/session_manager.py +319 -0
- higpertext/templates/env/generic-shell.yaml +21 -0
- higpertext/templates/env/node-vitest.yaml +27 -0
- higpertext/templates/env/python-pytest.yaml +29 -0
- higpertext/templates/html/commit_body.html +20 -0
- higpertext/templates/html/commit_diff.html +4 -0
- higpertext/templates/html/commit_index.html +29 -0
- higpertext/templates/html/commit_layer.html +11 -0
- higpertext/templates/html/commit_shell.html +28 -0
- higpertext/templates/html/graph_visualize.html +86 -0
- higpertext/templates/html/roadmap_body.html +12 -0
- higpertext/templates/html/roadmap_phase.html +5 -0
- higpertext/templates/html/roadmap_shell.html +29 -0
- higpertext/templates/markdown/commit_report.md +18 -0
- higpertext/templates/markdown/efficiency_report.md +12 -0
- higpertext/templates/markdown/roadmap_report.md +25 -0
- higpertext/templates/skills/best-practices.md +7 -0
- higpertext/templates/skills/clean-code.md +8 -0
- higpertext/templates/skills/ddd-standards.md +7 -0
- higpertext/templates/skills/tdd-practices.md +7 -0
- higpertext/templates/subagents/architect.md +7 -0
- higpertext/templates/subagents/test-engineer.md +7 -0
- higpertext/templates/workflows/build.json +23 -0
- higpertext/templates/workflows/compact.json +21 -0
- higpertext/templates/workflows/plan.json +59 -0
- higpertext/templates/workflows/review.json +26 -0
- higpertext/templates/workflows/spec.json +27 -0
- higpertext_cli-0.8.0.dist-info/METADATA +35 -0
- higpertext_cli-0.8.0.dist-info/RECORD +335 -0
- higpertext_cli-0.8.0.dist-info/WHEEL +5 -0
- higpertext_cli-0.8.0.dist-info/entry_points.txt +2 -0
- higpertext_cli-0.8.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""higpertext Resilience — Retry con backoff exponencial y Circuit Breaker por capability
|
|
2
|
+
(Infraestructura)."""
|
|
3
|
+
|
|
4
|
+
import time
|
|
5
|
+
import threading
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
9
|
+
from higpertext.kernel.infrastructure.logger import get_logger
|
|
10
|
+
_log = get_logger()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CircuitState(Enum):
|
|
14
|
+
CLOSED = "closed"
|
|
15
|
+
OPEN = "open"
|
|
16
|
+
HALF_OPEN = "half_open"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class CircuitBreaker:
|
|
21
|
+
failure_threshold: int = 3
|
|
22
|
+
recovery_timeout: float = 60.0
|
|
23
|
+
success_threshold: int = 1
|
|
24
|
+
|
|
25
|
+
_state: CircuitState = field(default=CircuitState.CLOSED, init=False, repr=False)
|
|
26
|
+
_failure_count: int = field(default=0, init=False, repr=False)
|
|
27
|
+
_success_count: int = field(default=0, init=False, repr=False)
|
|
28
|
+
_opened_at: float = field(default=0.0, init=False, repr=False)
|
|
29
|
+
_lock: threading.Lock = field(default_factory=threading.Lock, init=False, repr=False)
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def state(self) -> CircuitState:
|
|
33
|
+
with self._lock:
|
|
34
|
+
if self._state == CircuitState.OPEN:
|
|
35
|
+
if time.monotonic() - self._opened_at >= self.recovery_timeout:
|
|
36
|
+
self._state = CircuitState.HALF_OPEN
|
|
37
|
+
self._success_count = 0
|
|
38
|
+
return self._state
|
|
39
|
+
|
|
40
|
+
def is_allowed(self) -> bool:
|
|
41
|
+
return self.state != CircuitState.OPEN
|
|
42
|
+
|
|
43
|
+
def record_success(self) -> None:
|
|
44
|
+
with self._lock:
|
|
45
|
+
self._failure_count = 0
|
|
46
|
+
if self._state == CircuitState.HALF_OPEN:
|
|
47
|
+
self._success_count += 1
|
|
48
|
+
if self._success_count >= self.success_threshold:
|
|
49
|
+
self._state = CircuitState.CLOSED
|
|
50
|
+
_log.info("[Circuit] Circuito CERRADO — servicio recuperado.")
|
|
51
|
+
|
|
52
|
+
def record_failure(self) -> None:
|
|
53
|
+
with self._lock:
|
|
54
|
+
self._failure_count += 1
|
|
55
|
+
if self._state == CircuitState.HALF_OPEN:
|
|
56
|
+
self._state = CircuitState.OPEN
|
|
57
|
+
self._opened_at = time.monotonic()
|
|
58
|
+
_log.info(
|
|
59
|
+
f"[Circuit] Circuito ABIERTO nuevamente — reintentando en {
|
|
60
|
+
self.recovery_timeout}s."
|
|
61
|
+
)
|
|
62
|
+
elif (
|
|
63
|
+
self._state == CircuitState.CLOSED and self._failure_count >= self.failure_threshold
|
|
64
|
+
):
|
|
65
|
+
self._state = CircuitState.OPEN
|
|
66
|
+
self._opened_at = time.monotonic()
|
|
67
|
+
_log.error(
|
|
68
|
+
f"[Circuit] Circuito ABIERTO tras {
|
|
69
|
+
self._failure_count} fallos consecutivos — reintentando en {
|
|
70
|
+
self.recovery_timeout}s."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ResilienceManager:
|
|
75
|
+
DEFAULT_MAX_RETRIES = 3
|
|
76
|
+
DEFAULT_BASE_DELAY = 2.0
|
|
77
|
+
DEFAULT_MAX_DELAY = 30.0
|
|
78
|
+
DEFAULT_JITTER = 0.5
|
|
79
|
+
|
|
80
|
+
def __init__(self) -> None:
|
|
81
|
+
self._breakers: dict[str, CircuitBreaker] = {}
|
|
82
|
+
self._lock = threading.Lock()
|
|
83
|
+
|
|
84
|
+
def get_breaker(self, capability_id: str) -> CircuitBreaker:
|
|
85
|
+
with self._lock:
|
|
86
|
+
if capability_id not in self._breakers:
|
|
87
|
+
self._breakers[capability_id] = CircuitBreaker()
|
|
88
|
+
return self._breakers[capability_id]
|
|
89
|
+
|
|
90
|
+
def execute_with_resilience(
|
|
91
|
+
self,
|
|
92
|
+
capability_id: str,
|
|
93
|
+
fn,
|
|
94
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
95
|
+
base_delay: float = DEFAULT_BASE_DELAY,
|
|
96
|
+
max_delay: float = DEFAULT_MAX_DELAY,
|
|
97
|
+
):
|
|
98
|
+
import random
|
|
99
|
+
|
|
100
|
+
breaker = self.get_breaker(capability_id)
|
|
101
|
+
|
|
102
|
+
if not breaker.is_allowed():
|
|
103
|
+
timeout_s = f"{breaker.recovery_timeout:.0f}s"
|
|
104
|
+
_log.info(
|
|
105
|
+
f"[Circuit] Capability '{capability_id}' bloqueada — circuito ABIERTO."
|
|
106
|
+
f" Reintentando automáticamente en {timeout_s}."
|
|
107
|
+
)
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
for attempt in range(1, max_retries + 1):
|
|
111
|
+
result = fn()
|
|
112
|
+
|
|
113
|
+
if result is not None and result.returncode == 0:
|
|
114
|
+
breaker.record_success()
|
|
115
|
+
return result
|
|
116
|
+
|
|
117
|
+
breaker.record_failure()
|
|
118
|
+
|
|
119
|
+
if not breaker.is_allowed():
|
|
120
|
+
_log.info(f"[Retry] Circuito abierto — deteniendo reintentos para '{capability_id}'.")
|
|
121
|
+
return result
|
|
122
|
+
|
|
123
|
+
if attempt < max_retries:
|
|
124
|
+
delay = min(base_delay * (2 ** (attempt - 1)), max_delay)
|
|
125
|
+
jitter = delay * self.DEFAULT_JITTER * random.random()
|
|
126
|
+
wait = delay + jitter
|
|
127
|
+
_log.info(
|
|
128
|
+
f"[Retry] Intento {attempt}/{max_retries} falló para '{capability_id}'."
|
|
129
|
+
f" Reintentando en {wait:.1f}s..."
|
|
130
|
+
)
|
|
131
|
+
time.sleep(wait)
|
|
132
|
+
else:
|
|
133
|
+
_log.info(f"[Retry] Agotados {max_retries} intentos para '{capability_id}'.")
|
|
134
|
+
|
|
135
|
+
return result
|
|
136
|
+
|
|
137
|
+
def get_status(self) -> dict:
|
|
138
|
+
with self._lock:
|
|
139
|
+
return {
|
|
140
|
+
cap_id: {
|
|
141
|
+
"state": breaker.state.value,
|
|
142
|
+
"failures": breaker._failure_count,
|
|
143
|
+
}
|
|
144
|
+
for cap_id, breaker in self._breakers.items()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
_resilience_manager: ResilienceManager | None = None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def get_resilience_manager() -> ResilienceManager:
|
|
152
|
+
global _resilience_manager
|
|
153
|
+
if _resilience_manager is None:
|
|
154
|
+
_resilience_manager = ResilienceManager()
|
|
155
|
+
return _resilience_manager
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Implementaciones concretas de repositorios JSON (Infraestructura)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from higpertext.kernel.config_paths import WORKSPACE_DIR_NAME, FILE_EXTENSION
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from higpertext.kernel.application.ports import (
|
|
9
|
+
IProfileRepository,
|
|
10
|
+
ICapabilityRepository,
|
|
11
|
+
IWorkflowRepository,
|
|
12
|
+
ISessionRepository,
|
|
13
|
+
)
|
|
14
|
+
from higpertext.kernel.domain.entities import Profile, Capability, Workflow, Session
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FileProfileRepository(IProfileRepository):
|
|
18
|
+
def __init__(self, base_profiles_dir: Path):
|
|
19
|
+
self.profiles_dir = base_profiles_dir
|
|
20
|
+
|
|
21
|
+
def _get_custom_profiles_dir(self) -> Path:
|
|
22
|
+
return Path(os.getcwd()).resolve() / WORKSPACE_DIR_NAME / "profiles"
|
|
23
|
+
|
|
24
|
+
def _get_project_profiles_dir(self) -> Path:
|
|
25
|
+
return Path(os.getcwd()).resolve() / "src" / "config" / "profiles"
|
|
26
|
+
|
|
27
|
+
def _get_all_files(self) -> list[Path]:
|
|
28
|
+
pattern = f"*{FILE_EXTENSION}"
|
|
29
|
+
files = list(self.profiles_dir.glob(pattern))
|
|
30
|
+
for extra_dir in (
|
|
31
|
+
self._get_custom_profiles_dir(),
|
|
32
|
+
self._get_project_profiles_dir(),
|
|
33
|
+
):
|
|
34
|
+
if extra_dir.exists():
|
|
35
|
+
files.extend(extra_dir.glob(pattern))
|
|
36
|
+
return files
|
|
37
|
+
|
|
38
|
+
def load(self, name: str) -> Profile:
|
|
39
|
+
filename = f"{name}{FILE_EXTENSION}"
|
|
40
|
+
candidates = [
|
|
41
|
+
self._get_custom_profiles_dir() / filename,
|
|
42
|
+
self._get_project_profiles_dir() / filename,
|
|
43
|
+
self.profiles_dir / filename,
|
|
44
|
+
]
|
|
45
|
+
p = next((c for c in candidates if c.exists()), None)
|
|
46
|
+
if p is None:
|
|
47
|
+
raise FileNotFoundError(f"Perfil no encontrado: {name}")
|
|
48
|
+
data = json.loads(p.read_text(encoding="utf-8"))
|
|
49
|
+
return Profile(
|
|
50
|
+
name=data.get("name", name),
|
|
51
|
+
description=data.get("description", ""),
|
|
52
|
+
system_prompt=data.get("system_prompt", ""),
|
|
53
|
+
capabilities=data.get("capabilities", []),
|
|
54
|
+
session_skills=data.get("session_skills", []),
|
|
55
|
+
session_subagents=data.get("session_subagents", []),
|
|
56
|
+
governance_access=data.get("governance_access", False),
|
|
57
|
+
extends=data.get("extends"),
|
|
58
|
+
model=data.get("model"),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def list_all(self) -> list[str]:
|
|
62
|
+
seen = set()
|
|
63
|
+
result = []
|
|
64
|
+
for p in self._get_all_files():
|
|
65
|
+
if p.stem not in seen:
|
|
66
|
+
seen.add(p.stem)
|
|
67
|
+
result.append(p.stem)
|
|
68
|
+
return result
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class FileCapabilityRepository(ICapabilityRepository):
|
|
72
|
+
def __init__(self, base_capabilities_dir: Path):
|
|
73
|
+
self.caps_dir = base_capabilities_dir
|
|
74
|
+
|
|
75
|
+
def _get_custom_capabilities_dir(self) -> Path:
|
|
76
|
+
return Path(os.getcwd()).resolve() / WORKSPACE_DIR_NAME / "capabilities"
|
|
77
|
+
|
|
78
|
+
def _get_project_capabilities_dir(self) -> Path:
|
|
79
|
+
return Path(os.getcwd()).resolve() / "src" / "higpertext" / "capabilities"
|
|
80
|
+
|
|
81
|
+
def _get_all_files(self) -> list[Path]:
|
|
82
|
+
pattern = f"*{FILE_EXTENSION}"
|
|
83
|
+
files = list(self.caps_dir.rglob(pattern))
|
|
84
|
+
for extra_dir in (
|
|
85
|
+
self._get_custom_capabilities_dir(),
|
|
86
|
+
self._get_project_capabilities_dir(),
|
|
87
|
+
):
|
|
88
|
+
if extra_dir.exists() and extra_dir != self.caps_dir:
|
|
89
|
+
files.extend(extra_dir.rglob(pattern))
|
|
90
|
+
return files
|
|
91
|
+
|
|
92
|
+
def load(self, cap_id: str) -> Capability:
|
|
93
|
+
f = next(
|
|
94
|
+
(f for f in self._get_all_files() if f.stem == cap_id or cap_id.endswith(f".{f.stem}")),
|
|
95
|
+
None,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if not f or not f.exists():
|
|
99
|
+
raise FileNotFoundError(f"Capacidad no encontrada: {cap_id}")
|
|
100
|
+
data = json.loads(f.read_text(encoding="utf-8"))
|
|
101
|
+
return Capability(
|
|
102
|
+
id=data.get("id", cap_id),
|
|
103
|
+
version=data.get("version", "1.0.0"),
|
|
104
|
+
name=data.get("name", ""),
|
|
105
|
+
description=data.get("description", ""),
|
|
106
|
+
entrypoint=data.get("entrypoint", ""),
|
|
107
|
+
language=data.get("language", "python"),
|
|
108
|
+
parameters=data.get("parameters", []),
|
|
109
|
+
security=data.get("security", {}),
|
|
110
|
+
hook_task_id=data.get("hook_task_id"),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def list_all(self) -> list[Capability]:
|
|
114
|
+
caps = []
|
|
115
|
+
for f in self._get_all_files():
|
|
116
|
+
try:
|
|
117
|
+
data = json.loads(f.read_text(encoding="utf-8"))
|
|
118
|
+
caps.append(
|
|
119
|
+
Capability(
|
|
120
|
+
id=data.get("id", f.stem),
|
|
121
|
+
version=data.get("version", "1.0.0"),
|
|
122
|
+
name=data.get("name", ""),
|
|
123
|
+
description=data.get("description", ""),
|
|
124
|
+
entrypoint=data.get("entrypoint", ""),
|
|
125
|
+
language=data.get("language", "python"),
|
|
126
|
+
parameters=data.get("parameters", []),
|
|
127
|
+
security=data.get("security", {}),
|
|
128
|
+
hook_task_id=data.get("hook_task_id"),
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
except Exception: # nosec B110
|
|
132
|
+
pass
|
|
133
|
+
return caps
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class FileWorkflowRepository(IWorkflowRepository):
|
|
137
|
+
def __init__(self, workflows_dir: Path):
|
|
138
|
+
self.wf_dir = workflows_dir
|
|
139
|
+
|
|
140
|
+
def _get_project_workflows_dir(self) -> Path:
|
|
141
|
+
return Path(os.getcwd()).resolve() / "src" / "workflows"
|
|
142
|
+
|
|
143
|
+
def _get_all_workflow_files(self) -> list[Path]:
|
|
144
|
+
pattern = f"*{FILE_EXTENSION}"
|
|
145
|
+
files = list(self.wf_dir.glob(pattern)) if self.wf_dir.exists() else []
|
|
146
|
+
project_dir = self._get_project_workflows_dir()
|
|
147
|
+
if project_dir.exists() and project_dir != self.wf_dir:
|
|
148
|
+
files.extend(project_dir.glob(pattern))
|
|
149
|
+
return files
|
|
150
|
+
|
|
151
|
+
def list_all(self) -> list[Workflow]:
|
|
152
|
+
wfs = []
|
|
153
|
+
for f in self._get_all_workflow_files():
|
|
154
|
+
try:
|
|
155
|
+
data = json.loads(f.read_text(encoding="utf-8"))
|
|
156
|
+
wfs.append(
|
|
157
|
+
Workflow(
|
|
158
|
+
id=data.get("id", f.stem),
|
|
159
|
+
name=data.get("name", ""),
|
|
160
|
+
description=data.get("description", ""),
|
|
161
|
+
steps=data.get("steps", []),
|
|
162
|
+
required_profile=data.get("required_profile", ""),
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
except Exception: # nosec B110
|
|
166
|
+
pass
|
|
167
|
+
return wfs
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class FileSessionRepository(ISessionRepository):
|
|
171
|
+
def __init__(self, session_file: Path):
|
|
172
|
+
self.session_file = session_file
|
|
173
|
+
|
|
174
|
+
def load_active(self) -> Session | None:
|
|
175
|
+
if not self.session_file.exists():
|
|
176
|
+
return None
|
|
177
|
+
try:
|
|
178
|
+
data = json.loads(self.session_file.read_text(encoding="utf-8"))
|
|
179
|
+
return Session(
|
|
180
|
+
session_id=data.get("session_id", ""),
|
|
181
|
+
profile=data.get("profile", ""),
|
|
182
|
+
assistant=data.get("assistant", ""),
|
|
183
|
+
status=data.get("status", "inactive"),
|
|
184
|
+
created_at=data.get("created_at", ""),
|
|
185
|
+
active_skills=data.get("active_skills", []),
|
|
186
|
+
active_subagents=data.get("active_subagents", []),
|
|
187
|
+
tasks=data.get("tasks", []),
|
|
188
|
+
)
|
|
189
|
+
except Exception:
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
def save(self, session: Session) -> None:
|
|
193
|
+
self.session_file.parent.mkdir(parents=True, exist_ok=True)
|
|
194
|
+
data = {
|
|
195
|
+
"session_id": session.session_id,
|
|
196
|
+
"profile": session.profile,
|
|
197
|
+
"assistant": session.assistant,
|
|
198
|
+
"status": session.status,
|
|
199
|
+
"created_at": session.created_at,
|
|
200
|
+
"active_skills": session.active_skills,
|
|
201
|
+
"active_subagents": session.active_subagents,
|
|
202
|
+
"tasks": session.tasks,
|
|
203
|
+
}
|
|
204
|
+
self.session_file.write_text(
|
|
205
|
+
json.dumps(data, indent=4, ensure_ascii=False), encoding="utf-8"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def delete(self) -> None:
|
|
209
|
+
if self.session_file.exists():
|
|
210
|
+
try:
|
|
211
|
+
self.session_file.unlink()
|
|
212
|
+
except OSError: # nosec B110
|
|
213
|
+
pass
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Implementaciones de infraestructura para gobernanza."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
|
|
8
|
+
from higpertext.kernel.config_paths import WORKSPACE_DIR_NAME
|
|
9
|
+
from higpertext.kernel.domain.governance import Rule, RuleOverride, Scope, Severity, Verdict
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
_SECTION_RULES_PATH = Path(__file__).resolve().parents[3] / "config" / "governance" / "section_rules.json"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _load_section_rules() -> dict:
|
|
16
|
+
"""Carga section_rules.json una sola vez. Devuelve dict vacío si no existe."""
|
|
17
|
+
try:
|
|
18
|
+
return json.loads(_SECTION_RULES_PATH.read_text(encoding="utf-8"))
|
|
19
|
+
except (OSError, json.JSONDecodeError):
|
|
20
|
+
return {}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _rule_id(section: str, index: int) -> str:
|
|
24
|
+
return f"{section}.rule-{index:02d}"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ContractLoader:
|
|
28
|
+
"""Carga y mergea reglas de gobernanza global + perfil activo."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, project_root: Path) -> None:
|
|
31
|
+
self._root = project_root
|
|
32
|
+
self._contract_path = (
|
|
33
|
+
project_root / "src" / "config" / "governance" / "guidelines_contract.json"
|
|
34
|
+
)
|
|
35
|
+
_rules = _load_section_rules()
|
|
36
|
+
# section -> (Scope, Severity)
|
|
37
|
+
self._section_map: dict[str, tuple[Scope, Severity]] = {
|
|
38
|
+
section: (Scope(cfg["scope"]), Severity(cfg["severity"]))
|
|
39
|
+
for section, cfg in _rules.get("section_map", {}).items()
|
|
40
|
+
}
|
|
41
|
+
# frozenset of (section, index) pairs that are auto-checkable
|
|
42
|
+
self._automated_positions: frozenset[tuple[str, int]] = frozenset(
|
|
43
|
+
(entry["section"], entry["index"])
|
|
44
|
+
for entry in _rules.get("automated_rule_positions", [])
|
|
45
|
+
)
|
|
46
|
+
# (section, index) -> canonical rule ID
|
|
47
|
+
self._canonical_ids: dict[tuple[str, int], str] = {
|
|
48
|
+
(entry["section"], entry["index"]): entry["id"]
|
|
49
|
+
for entry in _rules.get("canonical_ids", [])
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
def load(self, profile: str = "") -> list[Rule]:
|
|
53
|
+
"""Retorna las reglas efectivas para el perfil dado."""
|
|
54
|
+
base_rules = self._load_base_rules()
|
|
55
|
+
if not profile:
|
|
56
|
+
return base_rules
|
|
57
|
+
|
|
58
|
+
overrides = self._load_profile_overrides(profile)
|
|
59
|
+
additions = self._load_profile_additions(profile)
|
|
60
|
+
|
|
61
|
+
merged = _merge_rules(base_rules, overrides)
|
|
62
|
+
return merged + additions
|
|
63
|
+
|
|
64
|
+
def load_raw_contract(self) -> dict:
|
|
65
|
+
if not self._contract_path.exists():
|
|
66
|
+
return {}
|
|
67
|
+
try:
|
|
68
|
+
data = json.loads(self._contract_path.read_text(encoding="utf-8"))
|
|
69
|
+
security = self._load_domain_file("security_guardrails")
|
|
70
|
+
if security:
|
|
71
|
+
data["security_guardrails"] = security
|
|
72
|
+
return data
|
|
73
|
+
except (OSError, json.JSONDecodeError):
|
|
74
|
+
return {}
|
|
75
|
+
|
|
76
|
+
def load_security_guardrails(self) -> dict:
|
|
77
|
+
return self._load_domain_file("security_guardrails")
|
|
78
|
+
|
|
79
|
+
def load_branching_strategy(self) -> dict:
|
|
80
|
+
return self._load_domain_file("branching_strategy")
|
|
81
|
+
|
|
82
|
+
def load_quality_gates(self) -> dict:
|
|
83
|
+
return self._load_domain_file("quality_gates")
|
|
84
|
+
|
|
85
|
+
def load_deployment_gates(self) -> dict:
|
|
86
|
+
return self._load_domain_file("deployment_gates")
|
|
87
|
+
|
|
88
|
+
def _load_domain_file(self, name: str) -> dict:
|
|
89
|
+
path = self._root / "src" / "config" / "governance" / f"{name}.json"
|
|
90
|
+
if not path.exists():
|
|
91
|
+
return {}
|
|
92
|
+
try:
|
|
93
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
94
|
+
except (OSError, json.JSONDecodeError):
|
|
95
|
+
return {}
|
|
96
|
+
|
|
97
|
+
def _load_base_rules(self) -> list[Rule]:
|
|
98
|
+
contract = self.load_raw_contract()
|
|
99
|
+
guidelines = contract.get("guidelines", {})
|
|
100
|
+
rules: list[Rule] = []
|
|
101
|
+
for section, entries in guidelines.items():
|
|
102
|
+
scope, severity = self._section_map.get(section, (Scope.ANY, Severity.MEDIUM))
|
|
103
|
+
for i, text in enumerate(entries, 1):
|
|
104
|
+
pos = (section, i)
|
|
105
|
+
rid = self._canonical_ids.get(pos, _rule_id(section, i))
|
|
106
|
+
automated = pos in self._automated_positions
|
|
107
|
+
rules.append(
|
|
108
|
+
Rule(
|
|
109
|
+
id=rid,
|
|
110
|
+
description=text,
|
|
111
|
+
severity=severity,
|
|
112
|
+
scopes=(scope,),
|
|
113
|
+
automated=automated,
|
|
114
|
+
source="global",
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
return rules
|
|
118
|
+
|
|
119
|
+
def _profile_governance_path(self, profile: str) -> Path:
|
|
120
|
+
return self._root / WORKSPACE_DIR_NAME / "governance" / f"{profile}.json"
|
|
121
|
+
|
|
122
|
+
def _load_profile_overrides(self, profile: str) -> list[RuleOverride]:
|
|
123
|
+
path = self._profile_governance_path(profile)
|
|
124
|
+
if not path.exists():
|
|
125
|
+
return []
|
|
126
|
+
try:
|
|
127
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
128
|
+
overrides = []
|
|
129
|
+
for rule_id, cfg in data.get("overrides", {}).items():
|
|
130
|
+
overrides.append(
|
|
131
|
+
RuleOverride(
|
|
132
|
+
rule_id=rule_id,
|
|
133
|
+
source=profile,
|
|
134
|
+
numeric_threshold=cfg.get("threshold"),
|
|
135
|
+
description=cfg.get("description"),
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
return overrides
|
|
139
|
+
except (OSError, json.JSONDecodeError):
|
|
140
|
+
return []
|
|
141
|
+
|
|
142
|
+
def _load_profile_additions(self, profile: str) -> list[Rule]:
|
|
143
|
+
path = self._profile_governance_path(profile)
|
|
144
|
+
if not path.exists():
|
|
145
|
+
return []
|
|
146
|
+
try:
|
|
147
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
148
|
+
rules: list[Rule] = []
|
|
149
|
+
for section, entries in data.get("additions", {}).items():
|
|
150
|
+
scope, severity = self._section_map.get(section, (Scope.ANY, Severity.MEDIUM))
|
|
151
|
+
for i, entry in enumerate(entries, 1):
|
|
152
|
+
if isinstance(entry, str):
|
|
153
|
+
text, cfg = entry, {}
|
|
154
|
+
else:
|
|
155
|
+
text = entry.get("rule", "")
|
|
156
|
+
cfg = entry
|
|
157
|
+
rid = cfg.get("id", f"{profile}.{section}.rule-{i:02d}")
|
|
158
|
+
rules.append(
|
|
159
|
+
Rule(
|
|
160
|
+
id=rid,
|
|
161
|
+
description=text,
|
|
162
|
+
severity=Severity(cfg.get("severity", severity.value)),
|
|
163
|
+
scopes=(Scope(cfg.get("scope", scope.value)),),
|
|
164
|
+
automated=cfg.get("automated", False),
|
|
165
|
+
capability=cfg.get("capability"),
|
|
166
|
+
source=profile,
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
return rules
|
|
170
|
+
except (OSError, json.JSONDecodeError):
|
|
171
|
+
return []
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _merge_rules(base: list[Rule], overrides: list[RuleOverride]) -> list[Rule]:
|
|
175
|
+
override_map = {o.rule_id: o for o in overrides}
|
|
176
|
+
return [rule.merge(override_map[rule.id]) if rule.id in override_map else rule for rule in base]
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class AuditLog:
|
|
180
|
+
def __init__(self, project_root: Path) -> None:
|
|
181
|
+
self._store = project_root / WORKSPACE_DIR_NAME / "state" / "governance_audit.jsonl"
|
|
182
|
+
|
|
183
|
+
def record(self, verdict: Verdict, profile: str, triggered_by: str = "") -> None:
|
|
184
|
+
entry = {
|
|
185
|
+
"ts": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|
186
|
+
"profile": profile,
|
|
187
|
+
"scope": verdict.scope,
|
|
188
|
+
"status": verdict.status.value,
|
|
189
|
+
"triggered_by": triggered_by,
|
|
190
|
+
"findings": len(verdict.findings),
|
|
191
|
+
"blocked": len(verdict.blocking_findings),
|
|
192
|
+
}
|
|
193
|
+
try:
|
|
194
|
+
self._store.parent.mkdir(parents=True, exist_ok=True)
|
|
195
|
+
with self._store.open("a", encoding="utf-8") as fh:
|
|
196
|
+
fh.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
|
197
|
+
except OSError:
|
|
198
|
+
pass
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Utilidad compartida para cargar y guardar hooks_config.json."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from higpertext.kernel.config_paths import WORKSPACE_DIR_NAME
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from higpertext.kernel.pkg_resources import pkg_data_root
|
|
8
|
+
|
|
9
|
+
_EMPTY: dict = {"hooks": [], "profile_hooks": {}, "webhooks": []}
|
|
10
|
+
|
|
11
|
+
# Ruta canónica del estado activo dentro del proyecto destino
|
|
12
|
+
_CONFIG_PATH = Path(WORKSPACE_DIR_NAME) / "config" / "hooks_config.json"
|
|
13
|
+
# Catálogo canónico de definiciones de hooks, empaquetado junto al código del engine
|
|
14
|
+
_CATALOG_REL_PATH = Path("hooks") / "hooks_catalog.json"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load_hooks_config(project_root: Path) -> dict:
|
|
18
|
+
"""Carga el estado activo del proyecto (profile_hooks, webhooks, hooks del proyecto)."""
|
|
19
|
+
config_path = project_root / _CONFIG_PATH
|
|
20
|
+
if not config_path.exists():
|
|
21
|
+
return dict(_EMPTY)
|
|
22
|
+
try:
|
|
23
|
+
return json.loads(config_path.read_text(encoding="utf-8"))
|
|
24
|
+
except (OSError, json.JSONDecodeError):
|
|
25
|
+
return dict(_EMPTY)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_hook_definitions(engine_root: Path) -> list[dict]:
|
|
29
|
+
"""Carga el catálogo canónico de definiciones de hooks del engine.
|
|
30
|
+
|
|
31
|
+
Busca primero en el árbol de fuente (modo dev), y si no existe ahí,
|
|
32
|
+
cae a los recursos empaquetados (higpertext_data) cuando se instala como CLI.
|
|
33
|
+
"""
|
|
34
|
+
candidates = [engine_root / "src" / "higpertext" / _CATALOG_REL_PATH]
|
|
35
|
+
pkg_root = pkg_data_root()
|
|
36
|
+
if pkg_root is not None:
|
|
37
|
+
candidates.append(pkg_root / "higpertext" / _CATALOG_REL_PATH)
|
|
38
|
+
|
|
39
|
+
for definitions_path in candidates:
|
|
40
|
+
if not definitions_path.exists():
|
|
41
|
+
continue
|
|
42
|
+
try:
|
|
43
|
+
data = json.loads(definitions_path.read_text(encoding="utf-8"))
|
|
44
|
+
return data.get("hooks", [])
|
|
45
|
+
except (OSError, json.JSONDecodeError):
|
|
46
|
+
continue
|
|
47
|
+
return []
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def save_hooks_config(project_root: Path, data: dict) -> None:
|
|
51
|
+
config_path = project_root / _CONFIG_PATH
|
|
52
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
config_path.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""WebhookDispatcher — emite eventos HTTP a sistemas externos sin dependencias extra."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import urllib.request
|
|
8
|
+
import urllib.error
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from higpertext.kernel.application.hook_registry import HookRegistry
|
|
12
|
+
|
|
13
|
+
_VAR_RE = re.compile(r"\$\{(\w+)\}")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _resolve_env(value: str) -> str:
|
|
17
|
+
return _VAR_RE.sub(lambda m: os.environ.get(m.group(1), ""), value)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _render_payload(template: dict, context: dict) -> dict:
|
|
21
|
+
"""Sustituye {{key}} en valores del template con valores del contexto."""
|
|
22
|
+
result: dict = {}
|
|
23
|
+
for k, v in template.items():
|
|
24
|
+
if isinstance(v, str):
|
|
25
|
+
for ctx_key, ctx_val in context.items():
|
|
26
|
+
v = v.replace(f"{{{{{ctx_key}}}}}", str(ctx_val))
|
|
27
|
+
result[k] = v
|
|
28
|
+
else:
|
|
29
|
+
result[k] = v
|
|
30
|
+
return result
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class WebhookDispatcher:
|
|
34
|
+
def __init__(self, project_root: Path) -> None:
|
|
35
|
+
self.registry = HookRegistry(project_root)
|
|
36
|
+
|
|
37
|
+
def dispatch(self, event: str, assistant: str, context: dict) -> None:
|
|
38
|
+
"""Emite webhooks activos para el evento dado."""
|
|
39
|
+
webhooks = self.registry.get_webhooks_for(assistant)
|
|
40
|
+
for wh in webhooks:
|
|
41
|
+
if wh.event != event:
|
|
42
|
+
continue
|
|
43
|
+
url = _resolve_env(wh.url)
|
|
44
|
+
if not url:
|
|
45
|
+
continue
|
|
46
|
+
payload = _render_payload(wh.payload_template, context)
|
|
47
|
+
self._post(url, payload, wh.timeout)
|
|
48
|
+
|
|
49
|
+
def _post(self, url: str, payload: dict, timeout: int) -> None:
|
|
50
|
+
body = json.dumps(payload).encode("utf-8")
|
|
51
|
+
req = urllib.request.Request(
|
|
52
|
+
url,
|
|
53
|
+
data=body,
|
|
54
|
+
headers={"Content-Type": "application/json"},
|
|
55
|
+
method="POST",
|
|
56
|
+
)
|
|
57
|
+
try:
|
|
58
|
+
with urllib.request.urlopen(req, timeout=timeout):
|
|
59
|
+
pass
|
|
60
|
+
except urllib.error.URLError: # nosec B110
|
|
61
|
+
pass # Webhook failures are non-blocking
|