algo-cli-runtime 0.14.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.
- algo_cli/__init__.py +3 -0
- algo_cli/__main__.py +7 -0
- algo_cli/_internal/__init__.py +12 -0
- algo_cli/_internal/policy_chain.py +259 -0
- algo_cli/action_registry.py +1047 -0
- algo_cli/agent_blocks.py +550 -0
- algo_cli/agent_pipeline.py +1457 -0
- algo_cli/agent_threads.py +308 -0
- algo_cli/animations.py +316 -0
- algo_cli/cache_admission.py +209 -0
- algo_cli/capability_mask.py +66 -0
- algo_cli/chat_protocol.py +116 -0
- algo_cli/chatgpt_auth.py +510 -0
- algo_cli/chatgpt_client.py +657 -0
- algo_cli/code_rag.py +479 -0
- algo_cli/config.py +651 -0
- algo_cli/context_budget.py +679 -0
- algo_cli/credential_helpers.py +315 -0
- algo_cli/deliberation.py +29 -0
- algo_cli/display.py +1470 -0
- algo_cli/evals/__init__.py +21 -0
- algo_cli/evals/algorithm_effectiveness.py +560 -0
- algo_cli/evals/competitive_harness_rating.py +702 -0
- algo_cli/evals/cot_quality.py +220 -0
- algo_cli/evals/harness_retrieval_benchmark.py +401 -0
- algo_cli/evals/performance_regression.py +136 -0
- algo_cli/evals/scorecard_grading.py +308 -0
- algo_cli/evals/session_distribution.py +84 -0
- algo_cli/execution_guardrails.py +806 -0
- algo_cli/extensions_manifest.py +84 -0
- algo_cli/git_evidence.py +227 -0
- algo_cli/google_workspace.py +407 -0
- algo_cli/google_workspace_auth.py +523 -0
- algo_cli/harness.py +2587 -0
- algo_cli/identity.py +557 -0
- algo_cli/index_compute_lab.py +228 -0
- algo_cli/inference_harness.py +70 -0
- algo_cli/intelligence/__init__.py +1103 -0
- algo_cli/intelligence/acrobat_config.py +307 -0
- algo_cli/intelligence/acrobat_manifests.py +338 -0
- algo_cli/intelligence/acrobat_models.py +195 -0
- algo_cli/intelligence/acrobat_pipeline.py +295 -0
- algo_cli/intelligence/acrobat_runtime.py +302 -0
- algo_cli/intelligence/acrobat_security.py +261 -0
- algo_cli/intelligence/acrobat_workflows.py +226 -0
- algo_cli/intelligence/actionability.py +165 -0
- algo_cli/intelligence/adversarial_audit.py +136 -0
- algo_cli/intelligence/agent_arena.py +92 -0
- algo_cli/intelligence/agent_benchmark.py +236 -0
- algo_cli/intelligence/agent_runtime.py +171 -0
- algo_cli/intelligence/agents_as_tools.py +70 -0
- algo_cli/intelligence/artifact_binding.py +80 -0
- algo_cli/intelligence/autonomous_engineer.py +1976 -0
- algo_cli/intelligence/backpressure.py +99 -0
- algo_cli/intelligence/bloom_filter.py +186 -0
- algo_cli/intelligence/bonferroni.py +66 -0
- algo_cli/intelligence/boundary_compaction.py +98 -0
- algo_cli/intelligence/catalog_verifier.py +172 -0
- algo_cli/intelligence/cavecrew.py +118 -0
- algo_cli/intelligence/changelog.py +176 -0
- algo_cli/intelligence/checkpoint_resume.py +92 -0
- algo_cli/intelligence/circuit_breaker.py +88 -0
- algo_cli/intelligence/clarification_gate.py +101 -0
- algo_cli/intelligence/code_graph.py +180 -0
- algo_cli/intelligence/coderank.py +97 -0
- algo_cli/intelligence/consistent_hash.py +150 -0
- algo_cli/intelligence/consortium_synthesis.py +139 -0
- algo_cli/intelligence/construction/__init__.py +241 -0
- algo_cli/intelligence/construction/common.py +273 -0
- algo_cli/intelligence/construction/documents.py +496 -0
- algo_cli/intelligence/construction/labor_units.py +1395 -0
- algo_cli/intelligence/construction/payments.py +470 -0
- algo_cli/intelligence/construction/risk.py +784 -0
- algo_cli/intelligence/content_extractor.py +132 -0
- algo_cli/intelligence/context_adaptive.py +102 -0
- algo_cli/intelligence/context_ops.py +95 -0
- algo_cli/intelligence/count_min.py +145 -0
- algo_cli/intelligence/cow_state.py +103 -0
- algo_cli/intelligence/critic_loop.py +119 -0
- algo_cli/intelligence/cross_source.py +113 -0
- algo_cli/intelligence/daemon_mode.py +99 -0
- algo_cli/intelligence/dag_orchestration.py +151 -0
- algo_cli/intelligence/deep_research.py +155 -0
- algo_cli/intelligence/degenerate_detector.py +78 -0
- algo_cli/intelligence/delta_report.py +92 -0
- algo_cli/intelligence/discovery_event_log.py +92 -0
- algo_cli/intelligence/document_ingest.py +298 -0
- algo_cli/intelligence/dual_layer_validate.py +151 -0
- algo_cli/intelligence/echo_fidelity.py +73 -0
- algo_cli/intelligence/ema_tuning.py +104 -0
- algo_cli/intelligence/event_log.py +92 -0
- algo_cli/intelligence/evidence_graph.py +114 -0
- algo_cli/intelligence/extension_host.py +162 -0
- algo_cli/intelligence/extension_manifest.py +115 -0
- algo_cli/intelligence/falsification_suite.py +178 -0
- algo_cli/intelligence/finance/__init__.py +169 -0
- algo_cli/intelligence/finance/anomalies.py +135 -0
- algo_cli/intelligence/finance/ap_ar.py +351 -0
- algo_cli/intelligence/finance/cash.py +162 -0
- algo_cli/intelligence/finance/close.py +332 -0
- algo_cli/intelligence/finance/common.py +244 -0
- algo_cli/intelligence/finance/construction.py +135 -0
- algo_cli/intelligence/finance/controls.py +172 -0
- algo_cli/intelligence/finance/evidence.py +119 -0
- algo_cli/intelligence/finance/exceptions.py +157 -0
- algo_cli/intelligence/finance/reconciliations.py +254 -0
- algo_cli/intelligence/finance/revenue.py +109 -0
- algo_cli/intelligence/finance/tax.py +74 -0
- algo_cli/intelligence/finance/workpapers.py +111 -0
- algo_cli/intelligence/finding_record.py +120 -0
- algo_cli/intelligence/flow_dag.py +267 -0
- algo_cli/intelligence/gatherer.py +223 -0
- algo_cli/intelligence/golden_master.py +98 -0
- algo_cli/intelligence/graph_rag.py +195 -0
- algo_cli/intelligence/group_chat.py +143 -0
- algo_cli/intelligence/hash_dedup.py +145 -0
- algo_cli/intelligence/hyperloglog.py +128 -0
- algo_cli/intelligence/incremental_index.py +316 -0
- algo_cli/intelligence/index_store.py +16 -0
- algo_cli/intelligence/iteration_plan.py +133 -0
- algo_cli/intelligence/kernel_plugins.py +167 -0
- algo_cli/intelligence/lesson_catalog.py +135 -0
- algo_cli/intelligence/llm_fallback.py +169 -0
- algo_cli/intelligence/log2_histogram.py +267 -0
- algo_cli/intelligence/lsp_integration.py +147 -0
- algo_cli/intelligence/memory_evolution.py +117 -0
- algo_cli/intelligence/minhash_lsh.py +182 -0
- algo_cli/intelligence/multi_model_score.py +174 -0
- algo_cli/intelligence/multi_tier_grade.py +211 -0
- algo_cli/intelligence/negative_controls.py +113 -0
- algo_cli/intelligence/numeric_clamp.py +63 -0
- algo_cli/intelligence/occ_editor.py +66 -0
- algo_cli/intelligence/output_normalize.py +112 -0
- algo_cli/intelligence/parallel_delegation.py +98 -0
- algo_cli/intelligence/parallel_fanout.py +104 -0
- algo_cli/intelligence/permission_modes.py +105 -0
- algo_cli/intelligence/pre_push_gate.py +68 -0
- algo_cli/intelligence/prefetch.py +171 -0
- algo_cli/intelligence/process_framework.py +217 -0
- algo_cli/intelligence/project_graph.py +387 -0
- algo_cli/intelligence/query_expansion.py +146 -0
- algo_cli/intelligence/ralph_loop.py +117 -0
- algo_cli/intelligence/rate_limiter.py +153 -0
- algo_cli/intelligence/refactor_transaction.py +94 -0
- algo_cli/intelligence/research_workspace.py +108 -0
- algo_cli/intelligence/retraction_ledger.py +72 -0
- algo_cli/intelligence/saga_pattern.py +88 -0
- algo_cli/intelligence/session_fork.py +100 -0
- algo_cli/intelligence/shadow_editor.py +67 -0
- algo_cli/intelligence/shell_session.py +213 -0
- algo_cli/intelligence/source_registry.py +143 -0
- algo_cli/intelligence/spawn_scales.py +99 -0
- algo_cli/intelligence/stat_stability.py +104 -0
- algo_cli/intelligence/structural_validator.py +148 -0
- algo_cli/intelligence/subagent_spawner.py +111 -0
- algo_cli/intelligence/symmetric_verify.py +70 -0
- algo_cli/intelligence/task_classifier.py +129 -0
- algo_cli/intelligence/team_execution.py +122 -0
- algo_cli/intelligence/tiered_access.py +121 -0
- algo_cli/intelligence/utility_registry.py +159 -0
- algo_cli/intuition_engine.py +560 -0
- algo_cli/intuition_injector.py +82 -0
- algo_cli/kernels/__init__.py +5 -0
- algo_cli/kernels/manifest.py +763 -0
- algo_cli/main.py +3903 -0
- algo_cli/memory_candidates.py +541 -0
- algo_cli/memory_echo_veil.py +394 -0
- algo_cli/memory_runtime.py +112 -0
- algo_cli/model_info.py +548 -0
- algo_cli/model_profile.py +160 -0
- algo_cli/model_routing.py +74 -0
- algo_cli/oneshot.py +331 -0
- algo_cli/perf_telemetry.py +389 -0
- algo_cli/plugins.py +245 -0
- algo_cli/private_event_store.py +654 -0
- algo_cli/quantization/__init__.py +24 -0
- algo_cli/quantization/lloyd_max.py +98 -0
- algo_cli/quantization/turbo_quant.py +308 -0
- algo_cli/reasoning/__init__.py +46 -0
- algo_cli/reasoning/combinatorial.py +356 -0
- algo_cli/reasoning/graph_of_thought.py +297 -0
- algo_cli/reasoning/mcts.py +220 -0
- algo_cli/reasoning/neuro_symbolic.py +250 -0
- algo_cli/reasoning/react.py +246 -0
- algo_cli/reasoning/reflexion.py +225 -0
- algo_cli/reasoning/tree_of_thought.py +241 -0
- algo_cli/reasoning_bridge.py +150 -0
- algo_cli/reconciliation.py +284 -0
- algo_cli/reflex.py +385 -0
- algo_cli/resources/docs/ALGO.md +13958 -0
- algo_cli/resources/docs/algo-cli-algorithm-evidence-contract.md +60 -0
- algo_cli/resources/docs/algo-cli-execution-verification-contract.md +59 -0
- algo_cli/resources/docs/algo-cli-memory-lifecycle-contract.md +72 -0
- algo_cli/resources/docs/harness-extension-cleanup-recommendation.md +41 -0
- algo_cli/resources/docs/index-compute-lab-integration.md +32 -0
- algo_cli/resources/docs/inference-harness-loop-blueprint-2026-06.md +55 -0
- algo_cli/resources/docs/main-split-map.md +35 -0
- algo_cli/resources/docs/privacy-and-context.md +48 -0
- algo_cli/resources/docs/reflex-loop-v0.2.md +354 -0
- algo_cli/resources/skills/README.md +26 -0
- algo_cli/resources/skills/algo-cli.md +59 -0
- algo_cli/resources/skills/edit-file-precision.md +49 -0
- algo_cli/resources/skills/harness-search-first.md +47 -0
- algo_cli/resources/skills/memory-recall-ritual.md +51 -0
- algo_cli/resources/skills/qol-algorithms.md +224 -0
- algo_cli/resources/skills/smart-error-recovery.md +56 -0
- algo_cli/resources/skills/tool-selection-cheatsheet.md +65 -0
- algo_cli/retrieval_algorithms.py +127 -0
- algo_cli/runtime_qos.py +236 -0
- algo_cli/runtime_services.py +320 -0
- algo_cli/session_commands.py +95 -0
- algo_cli/session_mode.py +113 -0
- algo_cli/skills.py +430 -0
- algo_cli/slash_dispatch.py +1265 -0
- algo_cli/small_context.py +206 -0
- algo_cli/spawn_budget.py +89 -0
- algo_cli/task_ledger.py +84 -0
- algo_cli/task_router.py +197 -0
- algo_cli/tool_context.py +94 -0
- algo_cli/tool_contract.py +99 -0
- algo_cli/tool_policy.py +357 -0
- algo_cli/tool_runtime.py +647 -0
- algo_cli/tools.py +3056 -0
- algo_cli/url_scheme.py +174 -0
- algo_cli/verify.py +154 -0
- algo_cli/version_manifest.py +178 -0
- algo_cli/vision_screenshot_verify.py +76 -0
- algo_cli/workspace_resolver.py +68 -0
- algo_cli/x_account.py +209 -0
- algo_cli/xai_auth.py +374 -0
- algo_cli/xai_client.py +600 -0
- algo_cli_runtime-0.14.0.dist-info/METADATA +369 -0
- algo_cli_runtime-0.14.0.dist-info/RECORD +237 -0
- algo_cli_runtime-0.14.0.dist-info/WHEEL +4 -0
- algo_cli_runtime-0.14.0.dist-info/entry_points.txt +3 -0
- algo_cli_runtime-0.14.0.dist-info/licenses/LICENSE +21 -0
- ollama_cli/__init__.py +67 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""B78. Incremental Structural Validation.
|
|
2
|
+
|
|
3
|
+
Validate code changes in <200ms using tree-sitter-style AST checks.
|
|
4
|
+
3-tier resolution. Circuit breaker for false positives.
|
|
5
|
+
Source: Keel pattern.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import ast
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from enum import Enum
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Severity(Enum):
|
|
15
|
+
ERROR = "ERROR"
|
|
16
|
+
WARNING = "WARNING"
|
|
17
|
+
INFO = "INFO"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Violation:
|
|
22
|
+
code: str
|
|
23
|
+
severity: Severity
|
|
24
|
+
symbol: str
|
|
25
|
+
file: str
|
|
26
|
+
line: int
|
|
27
|
+
message: str
|
|
28
|
+
fix_hint: str = ""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CircuitBreaker:
|
|
32
|
+
"""Auto-downgrade repeated false positives to warnings."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, max_failures: int = 3) -> None:
|
|
35
|
+
self._counts: dict[str, int] = {}
|
|
36
|
+
self._max = max_failures
|
|
37
|
+
|
|
38
|
+
def check(self, violation: Violation) -> Violation:
|
|
39
|
+
if violation.severity == Severity.ERROR:
|
|
40
|
+
key = f"{violation.code}:{violation.symbol}"
|
|
41
|
+
self._counts[key] = self._counts.get(key, 0) + 1
|
|
42
|
+
if self._counts[key] >= self._max:
|
|
43
|
+
violation.severity = Severity.WARNING
|
|
44
|
+
return violation
|
|
45
|
+
|
|
46
|
+
def reset(self) -> None:
|
|
47
|
+
self._counts.clear()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class StructuralValidator:
|
|
51
|
+
"""Validate Python code structure using AST."""
|
|
52
|
+
|
|
53
|
+
ERROR_CODES = {
|
|
54
|
+
"E001": "Symbol referenced but not found",
|
|
55
|
+
"E002": "Missing type hints",
|
|
56
|
+
"E003": "Missing docstring",
|
|
57
|
+
"E004": "Function removed but still referenced",
|
|
58
|
+
"E005": "Arity mismatch",
|
|
59
|
+
"W001": "Symbol in wrong module",
|
|
60
|
+
"W002": "Duplicate name across modules",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
def __init__(self) -> None:
|
|
64
|
+
self._breaker = CircuitBreaker()
|
|
65
|
+
self._defined_names: dict[str, str] = {} # name → file
|
|
66
|
+
self._referenced_names: set[str] = set()
|
|
67
|
+
|
|
68
|
+
def compile_file(self, filepath: str, content: str) -> list[Violation]:
|
|
69
|
+
"""Validate a single file. Returns violations."""
|
|
70
|
+
violations: list[Violation] = []
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
tree = ast.parse(content, filename=filepath)
|
|
74
|
+
except SyntaxError as e:
|
|
75
|
+
violations.append(Violation(
|
|
76
|
+
code="E001", severity=Severity.ERROR,
|
|
77
|
+
symbol="<syntax>", file=filepath, line=e.lineno or 0,
|
|
78
|
+
message=f"Syntax error: {e.msg}",
|
|
79
|
+
fix_hint="Fix the syntax error",
|
|
80
|
+
))
|
|
81
|
+
return violations
|
|
82
|
+
|
|
83
|
+
# Collect defined names
|
|
84
|
+
for node in ast.walk(tree):
|
|
85
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
86
|
+
self._defined_names[node.name] = filepath
|
|
87
|
+
# Check for type hints
|
|
88
|
+
if not node.returns and not any(a.annotation for a in node.args.args):
|
|
89
|
+
violations.append(Violation(
|
|
90
|
+
code="E002", severity=Severity.WARNING,
|
|
91
|
+
symbol=node.name, file=filepath, line=node.lineno,
|
|
92
|
+
message="Missing type hints",
|
|
93
|
+
fix_hint="Add type annotations",
|
|
94
|
+
))
|
|
95
|
+
# Check for docstring
|
|
96
|
+
if not ast.get_docstring(node):
|
|
97
|
+
violations.append(Violation(
|
|
98
|
+
code="E003", severity=Severity.INFO,
|
|
99
|
+
symbol=node.name, file=filepath, line=node.lineno,
|
|
100
|
+
message="Missing docstring",
|
|
101
|
+
fix_hint="Add a docstring",
|
|
102
|
+
))
|
|
103
|
+
elif isinstance(node, ast.ClassDef):
|
|
104
|
+
self._defined_names[node.name] = filepath
|
|
105
|
+
|
|
106
|
+
# Collect referenced names
|
|
107
|
+
for node in ast.walk(tree):
|
|
108
|
+
if isinstance(node, ast.Call):
|
|
109
|
+
if isinstance(node.func, ast.Name):
|
|
110
|
+
self._referenced_names.add(node.func.name)
|
|
111
|
+
|
|
112
|
+
# Check for broken callers
|
|
113
|
+
for ref in self._referenced_names:
|
|
114
|
+
if ref not in self._defined_names and not ref.startswith("_"):
|
|
115
|
+
# Only flag if it looks like a local reference
|
|
116
|
+
pass # Could be built-in or import
|
|
117
|
+
|
|
118
|
+
# Apply circuit breaker
|
|
119
|
+
violations = [self._breaker.check(v) for v in violations]
|
|
120
|
+
return violations
|
|
121
|
+
|
|
122
|
+
def check_broken_callers(self, removed_name: str) -> list[Violation]:
|
|
123
|
+
"""Check if a removed function is still referenced."""
|
|
124
|
+
if removed_name in self._referenced_names:
|
|
125
|
+
return [Violation(
|
|
126
|
+
code="E004", severity=Severity.ERROR,
|
|
127
|
+
symbol=removed_name, file="<unknown>", line=0,
|
|
128
|
+
message=f"Function '{removed_name}' removed but still referenced",
|
|
129
|
+
fix_hint="Update callers or restore function",
|
|
130
|
+
)]
|
|
131
|
+
return []
|
|
132
|
+
|
|
133
|
+
def find_duplicates(self) -> list[Violation]:
|
|
134
|
+
"""Find duplicate names across modules."""
|
|
135
|
+
name_to_files: dict[str, list[str]] = {}
|
|
136
|
+
for name, filepath in self._defined_names.items():
|
|
137
|
+
name_to_files.setdefault(name, []).append(filepath)
|
|
138
|
+
|
|
139
|
+
violations: list[Violation] = []
|
|
140
|
+
for name, files in name_to_files.items():
|
|
141
|
+
if len(set(files)) > 1:
|
|
142
|
+
violations.append(Violation(
|
|
143
|
+
code="W002", severity=Severity.WARNING,
|
|
144
|
+
symbol=name, file=files[0], line=0,
|
|
145
|
+
message=f"Duplicate name '{name}' in {len(set(files))} files",
|
|
146
|
+
fix_hint="Rename to avoid confusion",
|
|
147
|
+
))
|
|
148
|
+
return violations
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""B69. Sub-Agent Spawning with Virtual File Isolation.
|
|
2
|
+
|
|
3
|
+
Spawn focused sub-agents with fresh context, explicit file passing,
|
|
4
|
+
and tool scoping. Source: Sub-Agent-MCP pattern.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from enum import Enum, auto
|
|
11
|
+
from typing import Callable
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AgentState(Enum):
|
|
15
|
+
PENDING = auto()
|
|
16
|
+
RUNNING = auto()
|
|
17
|
+
DONE = auto()
|
|
18
|
+
FAILED = auto()
|
|
19
|
+
CANCELLED = auto()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class SubAgentConfig:
|
|
24
|
+
id: str
|
|
25
|
+
name: str
|
|
26
|
+
system_prompt: str = ""
|
|
27
|
+
model: str = "" # empty = use parent's model
|
|
28
|
+
allowed_tools: list[str] = field(default_factory=list)
|
|
29
|
+
allowed_files: list[str] = field(default_factory=list) # explicit file passing
|
|
30
|
+
max_iterations: int = 5
|
|
31
|
+
timeout_s: float = 60.0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class SubAgentResult:
|
|
36
|
+
agent_id: str
|
|
37
|
+
state: AgentState = AgentState.PENDING
|
|
38
|
+
output: str = ""
|
|
39
|
+
files_read: list[str] = field(default_factory=list)
|
|
40
|
+
files_written: list[str] = field(default_factory=list)
|
|
41
|
+
tool_calls: int = 0
|
|
42
|
+
duration_s: float = 0.0
|
|
43
|
+
error: str | None = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SubAgentSpawner:
|
|
47
|
+
"""Spawn sub-agents with virtual file isolation."""
|
|
48
|
+
|
|
49
|
+
def __init__(self) -> None:
|
|
50
|
+
self._configs: dict[str, SubAgentConfig] = {}
|
|
51
|
+
self._results: dict[str, SubAgentResult] = {}
|
|
52
|
+
self._active: dict[str, SubAgentResult] = {}
|
|
53
|
+
|
|
54
|
+
def register(self, config: SubAgentConfig) -> None:
|
|
55
|
+
self._configs[config.id] = config
|
|
56
|
+
|
|
57
|
+
def spawn(self, agent_id: str, task: str,
|
|
58
|
+
run_fn: Callable[[SubAgentConfig, str], str] | None = None) -> SubAgentResult:
|
|
59
|
+
config = self._configs.get(agent_id)
|
|
60
|
+
if not config:
|
|
61
|
+
raise KeyError(f"Agent {agent_id} not registered")
|
|
62
|
+
|
|
63
|
+
result = SubAgentResult(agent_id=agent_id, state=AgentState.RUNNING)
|
|
64
|
+
self._active[agent_id] = result
|
|
65
|
+
start = time.time()
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
if run_fn:
|
|
69
|
+
output = run_fn(config, task)
|
|
70
|
+
else:
|
|
71
|
+
output = f"[{config.name}: {task}]"
|
|
72
|
+
result.output = output
|
|
73
|
+
result.state = AgentState.DONE
|
|
74
|
+
except Exception as e:
|
|
75
|
+
result.error = str(e)
|
|
76
|
+
result.state = AgentState.FAILED
|
|
77
|
+
finally:
|
|
78
|
+
result.duration_s = time.time() - start
|
|
79
|
+
self._results[agent_id] = result
|
|
80
|
+
self._active.pop(agent_id, None)
|
|
81
|
+
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
def cancel(self, agent_id: str) -> bool:
|
|
85
|
+
result = self._active.get(agent_id)
|
|
86
|
+
if result and result.state == AgentState.RUNNING:
|
|
87
|
+
result.state = AgentState.CANCELLED
|
|
88
|
+
return True
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
def get_result(self, agent_id: str) -> SubAgentResult | None:
|
|
92
|
+
return self._results.get(agent_id)
|
|
93
|
+
|
|
94
|
+
def list_active(self) -> list[str]:
|
|
95
|
+
return [aid for aid, r in self._active.items() if r.state == AgentState.RUNNING]
|
|
96
|
+
|
|
97
|
+
def can_access_file(self, agent_id: str, file_path: str) -> bool:
|
|
98
|
+
config = self._configs.get(agent_id)
|
|
99
|
+
if not config:
|
|
100
|
+
return False
|
|
101
|
+
if not config.allowed_files:
|
|
102
|
+
return True # no restriction
|
|
103
|
+
return file_path in config.allowed_files
|
|
104
|
+
|
|
105
|
+
def can_use_tool(self, agent_id: str, tool_name: str) -> bool:
|
|
106
|
+
config = self._configs.get(agent_id)
|
|
107
|
+
if not config:
|
|
108
|
+
return False
|
|
109
|
+
if not config.allowed_tools:
|
|
110
|
+
return True
|
|
111
|
+
return tool_name in config.allowed_tools
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""H14 — Symmetric Encode/Decode Verification.
|
|
2
|
+
|
|
3
|
+
Every creator pattern needs a verifier companion.
|
|
4
|
+
Mined from ST3GG dual-use design.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class SymmetricPair:
|
|
13
|
+
"""A creator/verifier pair of algorithm entries."""
|
|
14
|
+
|
|
15
|
+
creator_id: str
|
|
16
|
+
verifier_id: str
|
|
17
|
+
creator_title: str = ""
|
|
18
|
+
verifier_title: str = ""
|
|
19
|
+
metadata: dict[str, field] = field(default_factory=dict)
|
|
20
|
+
|
|
21
|
+
def to_dict(self) -> dict[str, str]:
|
|
22
|
+
return {
|
|
23
|
+
"creator_id": self.creator_id,
|
|
24
|
+
"verifier_id": self.verifier_id,
|
|
25
|
+
"creator_title": self.creator_title,
|
|
26
|
+
"verifier_title": self.verifier_title,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SymmetricVerifier:
|
|
31
|
+
"""Check that every creator entry has a matching verifier entry."""
|
|
32
|
+
|
|
33
|
+
def __init__(self) -> None:
|
|
34
|
+
self._pairs: list[SymmetricPair] = []
|
|
35
|
+
|
|
36
|
+
def register(
|
|
37
|
+
self,
|
|
38
|
+
creator_id: str,
|
|
39
|
+
verifier_id: str,
|
|
40
|
+
creator_title: str = "",
|
|
41
|
+
verifier_title: str = "",
|
|
42
|
+
) -> SymmetricPair:
|
|
43
|
+
pair = SymmetricPair(
|
|
44
|
+
creator_id=creator_id,
|
|
45
|
+
verifier_id=verifier_id,
|
|
46
|
+
creator_title=creator_title,
|
|
47
|
+
verifier_title=verifier_title,
|
|
48
|
+
)
|
|
49
|
+
self._pairs.append(pair)
|
|
50
|
+
return pair
|
|
51
|
+
|
|
52
|
+
def check_symmetry(self, entry_ids: set[str]) -> list[str]:
|
|
53
|
+
"""Return list of unpaired creator IDs."""
|
|
54
|
+
missing = []
|
|
55
|
+
for pair in self._pairs:
|
|
56
|
+
if pair.creator_id in entry_ids and pair.verifier_id not in entry_ids:
|
|
57
|
+
missing.append(pair.creator_id)
|
|
58
|
+
return missing
|
|
59
|
+
|
|
60
|
+
def get_pairs(self) -> list[SymmetricPair]:
|
|
61
|
+
return list(self._pairs)
|
|
62
|
+
|
|
63
|
+
def count(self) -> int:
|
|
64
|
+
return len(self._pairs)
|
|
65
|
+
|
|
66
|
+
def find_pair(self, creator_id: str) -> SymmetricPair | None:
|
|
67
|
+
for pair in self._pairs:
|
|
68
|
+
if pair.creator_id == creator_id:
|
|
69
|
+
return pair
|
|
70
|
+
return None
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""B84. Task Classifier + Agent Delegator.
|
|
2
|
+
|
|
3
|
+
Classify tasks by complexity/domain. Route to appropriate agents.
|
|
4
|
+
Source: CCASP pattern.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from enum import Enum, auto
|
|
10
|
+
from typing import Callable
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TaskComplexity(Enum):
|
|
14
|
+
TRIVIAL = auto() # read, list, search — cheap model
|
|
15
|
+
SIMPLE = auto() # single edit, single test — standard model
|
|
16
|
+
MODERATE = auto() # multi-file edit, fix bug — standard model
|
|
17
|
+
COMPLEX = auto() # refactor, new feature — expensive model
|
|
18
|
+
CRITICAL = auto() # architecture, security — expensive model + review
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TaskDomain(Enum):
|
|
22
|
+
CODE = auto()
|
|
23
|
+
RESEARCH = auto()
|
|
24
|
+
MAINTENANCE = auto()
|
|
25
|
+
DOCUMENTATION = auto()
|
|
26
|
+
TESTING = auto()
|
|
27
|
+
DEPLOYMENT = auto()
|
|
28
|
+
UNKNOWN = auto()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class TaskClassification:
|
|
33
|
+
complexity: TaskComplexity
|
|
34
|
+
domain: TaskDomain
|
|
35
|
+
confidence: float = 0.5
|
|
36
|
+
suggested_model: str = ""
|
|
37
|
+
suggested_agents: list[str] = field(default_factory=list)
|
|
38
|
+
estimated_steps: int = 1
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
COMPLEXITY_KEYWORDS: dict[TaskComplexity, list[str]] = {
|
|
42
|
+
TaskComplexity.TRIVIAL: ["read", "list", "show", "find", "search", "grep", "where"],
|
|
43
|
+
TaskComplexity.SIMPLE: ["fix", "update", "rename", "add", "remove", "change"],
|
|
44
|
+
TaskComplexity.MODERATE: ["refactor", "migrate", "convert", "split", "merge"],
|
|
45
|
+
TaskComplexity.COMPLEX: ["implement", "design", "architect", "create new", "build"],
|
|
46
|
+
TaskComplexity.CRITICAL: ["security", "deploy", "release", "migration", "rewrite"],
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
DOMAIN_KEYWORDS: dict[TaskDomain, list[str]] = {
|
|
50
|
+
TaskDomain.CODE: ["function", "class", "method", "variable", "import", "module"],
|
|
51
|
+
TaskDomain.RESEARCH: ["research", "analyze", "compare", "investigate", "survey"],
|
|
52
|
+
TaskDomain.MAINTENANCE: ["maintain", "clean", "update", "upgrade", "fix"],
|
|
53
|
+
TaskDomain.DOCUMENTATION: ["document", "readme", "doc", "comment", "explain"],
|
|
54
|
+
TaskDomain.TESTING: ["test", "spec", "coverage", "mock", "fixture"],
|
|
55
|
+
TaskDomain.DEPLOYMENT: ["deploy", "release", "publish", "ci", "cd", "build"],
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
MODEL_BY_COMPLEXITY: dict[TaskComplexity, str] = {
|
|
59
|
+
TaskComplexity.TRIVIAL: "qwen3:4b",
|
|
60
|
+
TaskComplexity.SIMPLE: "glm-5.2",
|
|
61
|
+
TaskComplexity.MODERATE: "glm-5.2",
|
|
62
|
+
TaskComplexity.COMPLEX: "gpt-5.5",
|
|
63
|
+
TaskComplexity.CRITICAL: "gpt-5.5",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TaskClassifier:
|
|
68
|
+
"""Classify tasks by complexity and domain."""
|
|
69
|
+
|
|
70
|
+
def classify(self, task_description: str) -> TaskClassification:
|
|
71
|
+
desc_lower = task_description.lower()
|
|
72
|
+
|
|
73
|
+
# Determine complexity
|
|
74
|
+
complexity_scores: dict[TaskComplexity, int] = {}
|
|
75
|
+
for comp, keywords in COMPLEXITY_KEYWORDS.items():
|
|
76
|
+
score = sum(1 for kw in keywords if kw in desc_lower)
|
|
77
|
+
complexity_scores[comp] = score
|
|
78
|
+
|
|
79
|
+
complexity = max(complexity_scores, key=complexity_scores.get)
|
|
80
|
+
if complexity_scores[complexity] == 0:
|
|
81
|
+
complexity = TaskComplexity.SIMPLE # default
|
|
82
|
+
|
|
83
|
+
# Determine domain
|
|
84
|
+
domain_scores: dict[TaskDomain, int] = {}
|
|
85
|
+
for domain, keywords in DOMAIN_KEYWORDS.items():
|
|
86
|
+
score = sum(1 for kw in keywords if kw in desc_lower)
|
|
87
|
+
domain_scores[domain] = score
|
|
88
|
+
|
|
89
|
+
domain = max(domain_scores, key=domain_scores.get)
|
|
90
|
+
if domain_scores[domain] == 0:
|
|
91
|
+
domain = TaskDomain.UNKNOWN
|
|
92
|
+
|
|
93
|
+
# Estimate steps
|
|
94
|
+
step_count = desc_lower.count(" and ") + desc_lower.count(" then ") + 1
|
|
95
|
+
step_count = min(step_count, 10)
|
|
96
|
+
|
|
97
|
+
# Confidence based on keyword match strength
|
|
98
|
+
total_matches = sum(complexity_scores.values()) + sum(domain_scores.values())
|
|
99
|
+
confidence = min(1.0, total_matches / 5.0)
|
|
100
|
+
|
|
101
|
+
return TaskClassification(
|
|
102
|
+
complexity=complexity,
|
|
103
|
+
domain=domain,
|
|
104
|
+
confidence=confidence,
|
|
105
|
+
suggested_model=MODEL_BY_COMPLEXITY[complexity],
|
|
106
|
+
estimated_steps=step_count,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class AgentDelegator:
|
|
111
|
+
"""Route classified tasks to appropriate agents."""
|
|
112
|
+
|
|
113
|
+
def __init__(self) -> None:
|
|
114
|
+
self._handlers: dict[TaskDomain, Callable[[str, TaskClassification], str]] = {}
|
|
115
|
+
self._classifier = TaskClassifier()
|
|
116
|
+
|
|
117
|
+
def register_handler(self, domain: TaskDomain,
|
|
118
|
+
handler: Callable[[str, TaskClassification], str]) -> None:
|
|
119
|
+
self._handlers[domain] = handler
|
|
120
|
+
|
|
121
|
+
def delegate(self, task: str) -> tuple[TaskClassification, str]:
|
|
122
|
+
"""Classify and delegate a task. Returns (classification, result)."""
|
|
123
|
+
classification = self._classifier.classify(task)
|
|
124
|
+
handler = self._handlers.get(classification.domain)
|
|
125
|
+
if handler:
|
|
126
|
+
result = handler(task, classification)
|
|
127
|
+
else:
|
|
128
|
+
result = f"No handler for domain {classification.domain.name}"
|
|
129
|
+
return classification, result
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""B72. Team Execution: ALL vs RACE Strategies.
|
|
2
|
+
|
|
3
|
+
ALL: wait for all agents, compare results.
|
|
4
|
+
RACE: first-success wins with atomic lock.
|
|
5
|
+
Input sharding: split input across agents.
|
|
6
|
+
Source: gecko pattern.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from enum import Enum, auto
|
|
14
|
+
from typing import Any, Callable
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TeamStrategy(Enum):
|
|
18
|
+
ALL = auto() # wait for all, compare
|
|
19
|
+
RACE = auto() # first success wins
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class TeamMember:
|
|
24
|
+
agent_id: str
|
|
25
|
+
model: str = ""
|
|
26
|
+
role: str = "worker"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class TeamResult:
|
|
31
|
+
strategy: TeamStrategy
|
|
32
|
+
results: dict[str, Any] = field(default_factory=dict) # agent_id → output
|
|
33
|
+
winner: str | None = None
|
|
34
|
+
errors: dict[str, str] = field(default_factory=dict)
|
|
35
|
+
duration_s: float = 0.0
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TeamExecutor:
|
|
39
|
+
"""Execute tasks across a team of agents with ALL or RACE strategy."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, members: list[TeamMember]) -> None:
|
|
42
|
+
self._members = {m.agent_id: m for m in members}
|
|
43
|
+
self._lock = threading.Lock()
|
|
44
|
+
|
|
45
|
+
def execute_all(
|
|
46
|
+
self,
|
|
47
|
+
task: str,
|
|
48
|
+
run_fn: Callable[[str, str], Any], # (agent_id, task) → output
|
|
49
|
+
timeout: float = 60.0,
|
|
50
|
+
) -> TeamResult:
|
|
51
|
+
"""ALL strategy: run all agents, wait for all, return all results."""
|
|
52
|
+
result = TeamResult(strategy=TeamStrategy.ALL)
|
|
53
|
+
start = time.time()
|
|
54
|
+
threads: list[threading.Thread] = []
|
|
55
|
+
|
|
56
|
+
def worker(agent_id: str) -> None:
|
|
57
|
+
try:
|
|
58
|
+
output = run_fn(agent_id, task)
|
|
59
|
+
with self._lock:
|
|
60
|
+
result.results[agent_id] = output
|
|
61
|
+
except Exception as e:
|
|
62
|
+
with self._lock:
|
|
63
|
+
result.errors[agent_id] = str(e)
|
|
64
|
+
|
|
65
|
+
for member_id in self._members:
|
|
66
|
+
t = threading.Thread(target=worker, args=(member_id,))
|
|
67
|
+
threads.append(t)
|
|
68
|
+
t.start()
|
|
69
|
+
|
|
70
|
+
for t in threads:
|
|
71
|
+
t.join(timeout=timeout)
|
|
72
|
+
|
|
73
|
+
result.duration_s = time.time() - start
|
|
74
|
+
return result
|
|
75
|
+
|
|
76
|
+
def execute_race(
|
|
77
|
+
self,
|
|
78
|
+
task: str,
|
|
79
|
+
run_fn: Callable[[str, str], Any],
|
|
80
|
+
timeout: float = 60.0,
|
|
81
|
+
) -> TeamResult:
|
|
82
|
+
"""RACE strategy: first agent to succeed wins."""
|
|
83
|
+
result = TeamResult(strategy=TeamStrategy.RACE)
|
|
84
|
+
start = time.time()
|
|
85
|
+
won = threading.Event()
|
|
86
|
+
|
|
87
|
+
def worker(agent_id: str) -> None:
|
|
88
|
+
if won.is_set():
|
|
89
|
+
return
|
|
90
|
+
try:
|
|
91
|
+
output = run_fn(agent_id, task)
|
|
92
|
+
with self._lock:
|
|
93
|
+
if not won.is_set():
|
|
94
|
+
result.results[agent_id] = output
|
|
95
|
+
result.winner = agent_id
|
|
96
|
+
won.set()
|
|
97
|
+
except Exception as e:
|
|
98
|
+
with self._lock:
|
|
99
|
+
result.errors[agent_id] = str(e)
|
|
100
|
+
|
|
101
|
+
threads: list[threading.Thread] = []
|
|
102
|
+
for member_id in self._members:
|
|
103
|
+
t = threading.Thread(target=worker, args=(member_id,))
|
|
104
|
+
threads.append(t)
|
|
105
|
+
t.start()
|
|
106
|
+
|
|
107
|
+
won.wait(timeout=timeout)
|
|
108
|
+
for t in threads:
|
|
109
|
+
t.join(timeout=1.0)
|
|
110
|
+
|
|
111
|
+
result.duration_s = time.time() - start
|
|
112
|
+
return result
|
|
113
|
+
|
|
114
|
+
def shard_input(self, items: list[Any], num_shards: int | None = None) -> list[list[Any]]:
|
|
115
|
+
"""Split input items across team members."""
|
|
116
|
+
n = num_shards or len(self._members)
|
|
117
|
+
if n <= 0:
|
|
118
|
+
return [items]
|
|
119
|
+
shards: list[list[Any]] = [[] for _ in range(n)]
|
|
120
|
+
for i, item in enumerate(items):
|
|
121
|
+
shards[i % n].append(item)
|
|
122
|
+
return [s for s in shards if s]
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""H31 — Tiered Tool Access.
|
|
2
|
+
|
|
3
|
+
Default / opt-in / approval-gated tool tiers.
|
|
4
|
+
Mined from T3MP3ST FEATURES.md §6.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AccessTier(str, Enum):
|
|
15
|
+
DEFAULT = "default"
|
|
16
|
+
OPT_IN = "opt_in"
|
|
17
|
+
APPROVAL_GATED = "approval_gated"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class TierConfig:
|
|
22
|
+
"""Configuration for a tool's access tier."""
|
|
23
|
+
|
|
24
|
+
tool_name: str
|
|
25
|
+
tier: AccessTier
|
|
26
|
+
env_var: str = ""
|
|
27
|
+
requires_approval: bool = False
|
|
28
|
+
description: str = ""
|
|
29
|
+
|
|
30
|
+
def to_dict(self) -> dict[str, Any]:
|
|
31
|
+
return {
|
|
32
|
+
"tool_name": self.tool_name,
|
|
33
|
+
"tier": self.tier.value,
|
|
34
|
+
"env_var": self.env_var,
|
|
35
|
+
"requires_approval": self.requires_approval,
|
|
36
|
+
"description": self.description,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class AccessResult:
|
|
42
|
+
"""Result of an access check."""
|
|
43
|
+
|
|
44
|
+
granted: bool
|
|
45
|
+
reason: str = ""
|
|
46
|
+
required_tier: AccessTier = AccessTier.DEFAULT
|
|
47
|
+
missing_env_var: str = ""
|
|
48
|
+
|
|
49
|
+
def to_dict(self) -> dict[str, Any]:
|
|
50
|
+
return {
|
|
51
|
+
"granted": self.granted,
|
|
52
|
+
"reason": self.reason,
|
|
53
|
+
"required_tier": self.required_tier.value,
|
|
54
|
+
"missing_env_var": self.missing_env_var,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class TieredAccessGate:
|
|
59
|
+
"""Check tool access against tiered configuration."""
|
|
60
|
+
|
|
61
|
+
def __init__(self) -> None:
|
|
62
|
+
self._configs: dict[str, TierConfig] = {}
|
|
63
|
+
|
|
64
|
+
def register(
|
|
65
|
+
self,
|
|
66
|
+
tool_name: str,
|
|
67
|
+
tier: AccessTier,
|
|
68
|
+
env_var: str = "",
|
|
69
|
+
requires_approval: bool = False,
|
|
70
|
+
description: str = "",
|
|
71
|
+
) -> TierConfig:
|
|
72
|
+
config = TierConfig(
|
|
73
|
+
tool_name=tool_name,
|
|
74
|
+
tier=tier,
|
|
75
|
+
env_var=env_var,
|
|
76
|
+
requires_approval=requires_approval,
|
|
77
|
+
description=description,
|
|
78
|
+
)
|
|
79
|
+
self._configs[tool_name] = config
|
|
80
|
+
return config
|
|
81
|
+
|
|
82
|
+
def check(self, tool_name: str, approved: bool = False) -> AccessResult:
|
|
83
|
+
config = self._configs.get(tool_name)
|
|
84
|
+
if config is None:
|
|
85
|
+
return AccessResult(granted=True, reason="No tier config — default allow")
|
|
86
|
+
if config.tier == AccessTier.DEFAULT:
|
|
87
|
+
return AccessResult(granted=True, reason="Default tier — always allowed")
|
|
88
|
+
if config.tier == AccessTier.OPT_IN:
|
|
89
|
+
if config.env_var and not os.environ.get(config.env_var):
|
|
90
|
+
return AccessResult(
|
|
91
|
+
granted=False,
|
|
92
|
+
reason=f"Opt-in tool requires env var {config.env_var}",
|
|
93
|
+
required_tier=AccessTier.OPT_IN,
|
|
94
|
+
missing_env_var=config.env_var,
|
|
95
|
+
)
|
|
96
|
+
return AccessResult(granted=True, reason="Opt-in env var set")
|
|
97
|
+
if config.tier == AccessTier.APPROVAL_GATED:
|
|
98
|
+
if config.env_var and not os.environ.get(config.env_var):
|
|
99
|
+
return AccessResult(
|
|
100
|
+
granted=False,
|
|
101
|
+
reason=f"Approval-gated tool requires env var {config.env_var}",
|
|
102
|
+
required_tier=AccessTier.APPROVAL_GATED,
|
|
103
|
+
missing_env_var=config.env_var,
|
|
104
|
+
)
|
|
105
|
+
if not approved:
|
|
106
|
+
return AccessResult(
|
|
107
|
+
granted=False,
|
|
108
|
+
reason="Approval-gated tool requires explicit approval",
|
|
109
|
+
required_tier=AccessTier.APPROVAL_GATED,
|
|
110
|
+
)
|
|
111
|
+
return AccessResult(granted=True, reason="Approved and env var set")
|
|
112
|
+
return AccessResult(granted=False, reason="Unknown tier")
|
|
113
|
+
|
|
114
|
+
def get_config(self, tool_name: str) -> TierConfig | None:
|
|
115
|
+
return self._configs.get(tool_name)
|
|
116
|
+
|
|
117
|
+
def all_configs(self) -> list[TierConfig]:
|
|
118
|
+
return list(self._configs.values())
|
|
119
|
+
|
|
120
|
+
def count(self) -> int:
|
|
121
|
+
return len(self._configs)
|