multiagentos 0.4.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.
Files changed (122) hide show
  1. agents/__init__.py +1 -0
  2. agents/catalog.py +52 -0
  3. agents/registry.py +11 -0
  4. core/__init__.py +38 -0
  5. core/artifacts.py +47 -0
  6. core/chat_agent_bridge.py +280 -0
  7. core/chat_agent_registry.py +61 -0
  8. core/chat_agent_router.py +85 -0
  9. core/chat_session.py +62 -0
  10. core/contracts/__init__.py +22 -0
  11. core/contracts/agent.py +21 -0
  12. core/contracts/ai.py +25 -0
  13. core/contracts/capability.py +44 -0
  14. core/contracts/chat_agent.py +34 -0
  15. core/contracts/checkpoint.py +78 -0
  16. core/contracts/discovery.py +30 -0
  17. core/contracts/execution.py +28 -0
  18. core/contracts/github.py +74 -0
  19. core/contracts/handoff.py +56 -0
  20. core/contracts/health.py +206 -0
  21. core/contracts/human_review.py +20 -0
  22. core/contracts/ide.py +113 -0
  23. core/contracts/mcp.py +76 -0
  24. core/contracts/model_runtime.py +25 -0
  25. core/contracts/planning.py +25 -0
  26. core/contracts/profile.py +66 -0
  27. core/contracts/quota.py +62 -0
  28. core/contracts/repository.py +34 -0
  29. core/contracts/resume.py +55 -0
  30. core/contracts/runtime.py +19 -0
  31. core/contracts/vyrelon_runtime.py +118 -0
  32. core/contracts/work_unit.py +48 -0
  33. core/delegation.py +33 -0
  34. core/handoff.py +68 -0
  35. core/lifecycle.py +85 -0
  36. core/multi_agent_workflow.py +574 -0
  37. core/orchestrator.py +51 -0
  38. core/planning.py +22 -0
  39. core/registry.py +44 -0
  40. core/routing.py +195 -0
  41. core/state.py +155 -0
  42. installer/__init__.py +1 -0
  43. installer/init.py +71 -0
  44. integrations/__init__.py +0 -0
  45. integrations/github/__init__.py +3 -0
  46. integrations/github/gateway.py +164 -0
  47. integrations/openai/__init__.py +1 -0
  48. integrations/openai/chat_agent.py +147 -0
  49. multiagentos/__init__.py +1 -0
  50. multiagentos/cli.py +759 -0
  51. multiagentos-0.4.0.dist-info/METADATA +7 -0
  52. multiagentos-0.4.0.dist-info/RECORD +122 -0
  53. multiagentos-0.4.0.dist-info/WHEEL +5 -0
  54. multiagentos-0.4.0.dist-info/entry_points.txt +2 -0
  55. multiagentos-0.4.0.dist-info/top_level.txt +7 -0
  56. profiles/__init__.py +1 -0
  57. profiles/common.py +11 -0
  58. profiles/detector.py +67 -0
  59. profiles/registry.py +46 -0
  60. profiles/resolver.py +63 -0
  61. runtime/__init__.py +5 -0
  62. runtime/agent/__init__.py +1 -0
  63. runtime/agent/ide.py +74 -0
  64. runtime/agent/model.py +197 -0
  65. runtime/agent/model_backed.py +49 -0
  66. runtime/agent/process.py +21 -0
  67. runtime/builtin_tools.py +57 -0
  68. runtime/capability.py +143 -0
  69. runtime/chat_adapter_registry.py +43 -0
  70. runtime/chat_config.py +57 -0
  71. runtime/chat_execution.py +52 -0
  72. runtime/config/__init__.py +1 -0
  73. runtime/config/loader.py +67 -0
  74. runtime/discovery.py +334 -0
  75. runtime/execution_config.py +71 -0
  76. runtime/execution_registry.py +50 -0
  77. runtime/git.py +84 -0
  78. runtime/github.py +56 -0
  79. runtime/github_probe.py +23 -0
  80. runtime/health.py +14 -0
  81. runtime/ide/__init__.py +6 -0
  82. runtime/ide/bridge.py +198 -0
  83. runtime/ide/registry.py +24 -0
  84. runtime/ide/runtime.py +35 -0
  85. runtime/local/__init__.py +18 -0
  86. runtime/local/filesystem.py +58 -0
  87. runtime/local/patch.py +79 -0
  88. runtime/local/path_security.py +31 -0
  89. runtime/local/permissions.py +20 -0
  90. runtime/local/process.py +56 -0
  91. runtime/local/shell.py +97 -0
  92. runtime/mcp/__init__.py +7 -0
  93. runtime/mcp/client.py +120 -0
  94. runtime/mcp/config.py +36 -0
  95. runtime/mcp/policy.py +57 -0
  96. runtime/mcp/proxy.py +30 -0
  97. runtime/mcp/server.py +137 -0
  98. runtime/mcp/session.py +19 -0
  99. runtime/model/__init__.py +5 -0
  100. runtime/model/ai_runtime.py +165 -0
  101. runtime/model/cli.py +49 -0
  102. runtime/model/config.py +98 -0
  103. runtime/model/credentials.py +42 -0
  104. runtime/model/factory.py +82 -0
  105. runtime/model/http.py +78 -0
  106. runtime/model/invoker.py +16 -0
  107. runtime/model/materializer.py +101 -0
  108. runtime/model/native.py +244 -0
  109. runtime/model/providers.py +55 -0
  110. runtime/model/registry.py +26 -0
  111. runtime/model/runtime_loader.py +51 -0
  112. runtime/model_control.py +136 -0
  113. runtime/multi_agent.py +142 -0
  114. runtime/policy.py +28 -0
  115. runtime/process.py +41 -0
  116. runtime/quota.py +259 -0
  117. runtime/repository.py +103 -0
  118. runtime/repository_tools.py +62 -0
  119. runtime/status.py +87 -0
  120. runtime/tool_calling.py +134 -0
  121. runtime/validation.py +121 -0
  122. runtime/vyrelon.py +1557 -0
runtime/discovery.py ADDED
@@ -0,0 +1,334 @@
1
+ """Provider-neutral discovery adapters for model capabilities and quota."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import urllib.request
8
+ from urllib.error import HTTPError, URLError
9
+ from datetime import datetime, timezone
10
+ from typing import Any, Callable
11
+
12
+ from core.contracts.ai import AIProvider, ModelSpec
13
+ from core.contracts.capability import CapabilityConfidence, ModelCapabilityProfile
14
+ from core.contracts.discovery import DiscoveryError, ProviderDiscoveryResult
15
+ from core.contracts.quota import QuotaConfidence, QuotaDimension, QuotaSnapshot
16
+ from runtime.policy import ExecutionPolicy
17
+
18
+
19
+ def _path(value: object, path: tuple[str, ...]) -> object:
20
+ current = value
21
+ for part in path:
22
+ if not isinstance(current, dict):
23
+ return None
24
+ current = current.get(part)
25
+ return current
26
+
27
+
28
+ def _string_list(value: object) -> frozenset[str]:
29
+ if not isinstance(value, list):
30
+ return frozenset()
31
+ return frozenset(str(item) for item in value if isinstance(item, (str, int, float)))
32
+
33
+
34
+ def _now() -> datetime:
35
+ return datetime.now(timezone.utc)
36
+
37
+
38
+ class ProviderDiscoveryAdapter:
39
+ """Discover provider metadata using declarative provider configuration."""
40
+
41
+ def __init__(
42
+ self,
43
+ policy: ExecutionPolicy | None = None,
44
+ fetch: Callable[[str, dict[str, str]], dict[str, Any] | tuple[dict[str, Any], dict[str, str]]] | None = None,
45
+ ) -> None:
46
+ self.policy = policy or ExecutionPolicy()
47
+ self.fetch = fetch or self._fetch
48
+
49
+ def discover(
50
+ self,
51
+ provider: AIProvider,
52
+ *,
53
+ models: tuple[ModelSpec, ...] | None = None,
54
+ ) -> ProviderDiscoveryResult:
55
+ metadata = provider.metadata
56
+ discovery = dict(metadata.get("discovery", {}) or {})
57
+ self._apply_builtin_defaults(provider, discovery)
58
+ if not isinstance(discovery, dict):
59
+ return ProviderDiscoveryResult(provider.id, source="model_spec")
60
+
61
+ endpoint = discovery.get("endpoint")
62
+ if not isinstance(endpoint, str) or not endpoint:
63
+ return ProviderDiscoveryResult(provider.id, source="model_spec")
64
+
65
+ headers = self._headers(discovery)
66
+ self._query_env = discovery.get("query_env", {})
67
+ fetched = self.fetch(endpoint, headers)
68
+ response_headers = {}
69
+ if isinstance(fetched, tuple):
70
+ payload, response_headers = fetched
71
+ else:
72
+ payload = fetched
73
+ model_payload = self._extract(payload, discovery.get("models_path", ["data"]))
74
+ if provider.kind.lower() == "gemini" and isinstance(model_payload, list):
75
+ model_payload = [self._normalize_gemini_model(item) for item in model_payload]
76
+ quota_payload = self._extract(payload, discovery.get("quota_path", []))
77
+
78
+ configured = {model.id: model for model in (models or provider.models)}
79
+ profiles = self._capabilities(provider, model_payload, configured)
80
+ quotas = self._quotas(provider, quota_payload, configured)
81
+ quotas.extend(self._header_quotas(provider, response_headers, configured))
82
+ return ProviderDiscoveryResult(
83
+ provider_id=provider.id,
84
+ capabilities=tuple(profiles),
85
+ quotas=tuple(quotas),
86
+ source="provider_discovery",
87
+ metadata={"endpoint": endpoint},
88
+ )
89
+
90
+ def discover_safe(
91
+ self,
92
+ provider: AIProvider,
93
+ *,
94
+ models: tuple[ModelSpec, ...] | None = None,
95
+ ) -> tuple[ProviderDiscoveryResult | None, DiscoveryError | None]:
96
+ try:
97
+ return self.discover(provider, models=models), None
98
+ except (OSError, ValueError, RuntimeError) as exc:
99
+ return None, DiscoveryError(
100
+ provider_id=provider.id,
101
+ operation="discover",
102
+ message=str(exc),
103
+ retryable=isinstance(exc, OSError),
104
+ )
105
+
106
+ @staticmethod
107
+ def _apply_builtin_defaults(provider: AIProvider, discovery: dict[str, object]) -> None:
108
+ if discovery.get("endpoint"):
109
+ return
110
+ presets = {
111
+ "openai": ("https://api.openai.com/v1/models", "OPENAI_API_KEY", "openai"),
112
+ "anthropic": ("https://api.anthropic.com/v1/models", "ANTHROPIC_API_KEY", "anthropic"),
113
+ "gemini": ("https://generativelanguage.googleapis.com/v1beta/models", "GEMINI_API_KEY", "gemini"),
114
+ }
115
+ preset = presets.get(provider.kind.lower()) or presets.get(provider.id.lower())
116
+ if not preset:
117
+ return
118
+ endpoint, key_env, kind = preset
119
+ discovery["endpoint"] = endpoint
120
+ discovery["models_path"] = ["models"] if kind == "gemini" else ["data"]
121
+ if kind == "gemini":
122
+ discovery["query_env"] = {"key": key_env}
123
+ elif kind == "openai":
124
+ discovery["header_env"] = {"Authorization": key_env}
125
+ discovery["header_prefix"] = {"Authorization": "Bearer "}
126
+ else:
127
+ discovery["header_env"] = {"x-api-key": key_env}
128
+ discovery["headers"] = {"anthropic-version": "2023-06-01"}
129
+
130
+ @staticmethod
131
+ def _header_quotas(provider: AIProvider, headers: dict[str, str], configured: dict[str, ModelSpec]) -> list[QuotaSnapshot]:
132
+ mappings = {
133
+ "openai": (
134
+ ("requests", "x-ratelimit-limit-requests", "x-ratelimit-remaining-requests"),
135
+ ("tokens", "x-ratelimit-limit-tokens", "x-ratelimit-remaining-tokens"),
136
+ ),
137
+ "anthropic": (
138
+ ("requests", "anthropic-ratelimit-requests-limit", "anthropic-ratelimit-requests-remaining"),
139
+ ("input_tokens", "anthropic-ratelimit-input-tokens-limit", "anthropic-ratelimit-input-tokens-remaining"),
140
+ ("output_tokens", "anthropic-ratelimit-output-tokens-limit", "anthropic-ratelimit-output-tokens-remaining"),
141
+ ),
142
+ }
143
+ rows = mappings.get(provider.kind.lower(), ())
144
+ dimensions = []
145
+ for name, limit_key, remaining_key in rows:
146
+ limit, remaining = _as_int(headers.get(limit_key)), _as_int(headers.get(remaining_key))
147
+ if limit is None and remaining is None:
148
+ continue
149
+ dimensions.append(QuotaDimension(name=name, limit=limit, used=(limit - remaining) if limit is not None and remaining is not None else None,
150
+ remaining=remaining, confidence=QuotaConfidence.ACTUAL, source="provider_response_headers"))
151
+ if not dimensions:
152
+ return []
153
+ return [QuotaSnapshot(model_id=model_id, provider_id=provider.id, observed_at=_now(), dimensions=tuple(dimensions),
154
+ scope="provider", metadata={"source": "provider_response_headers"}) for model_id in configured]
155
+
156
+ @staticmethod
157
+ def _normalize_gemini_model(item: object) -> object:
158
+ if not isinstance(item, dict):
159
+ return item
160
+ normalized = dict(item)
161
+ name = normalized.get("name")
162
+ if isinstance(name, str) and name.startswith("models/"):
163
+ normalized["id"] = name[len("models/"):]
164
+ methods = normalized.get("supportedGenerationMethods")
165
+ if isinstance(methods, list):
166
+ normalized["capabilities"] = ["chat" if str(method) == "generateContent" else str(method) for method in methods]
167
+ return normalized
168
+
169
+ def _capabilities(
170
+ self,
171
+ provider: AIProvider,
172
+ value: object,
173
+ configured: dict[str, ModelSpec],
174
+ ) -> list[ModelCapabilityProfile]:
175
+ if not isinstance(value, list):
176
+ return []
177
+ profiles: list[ModelCapabilityProfile] = []
178
+ for item in value:
179
+ if not isinstance(item, dict):
180
+ continue
181
+ model_id = str(item.get("id", ""))
182
+ if not model_id:
183
+ continue
184
+ base = configured.get(model_id)
185
+ capabilities = set(base.capabilities if base else ())
186
+ capabilities.update(_normalize_capabilities(item))
187
+ profiles.append(
188
+ ModelCapabilityProfile(
189
+ model_id=model_id,
190
+ provider_id=provider.id,
191
+ capabilities=frozenset(capabilities),
192
+ confidence=CapabilityConfidence.DECLARED,
193
+ source="provider_discovery",
194
+ metadata={"provider_metadata": item},
195
+ )
196
+ )
197
+ return profiles
198
+
199
+ def _quotas(
200
+ self,
201
+ provider: AIProvider,
202
+ value: object,
203
+ configured: dict[str, ModelSpec],
204
+ ) -> list[QuotaSnapshot]:
205
+ if not isinstance(value, list):
206
+ return []
207
+ snapshots: list[QuotaSnapshot] = []
208
+ for item in value:
209
+ if not isinstance(item, dict):
210
+ continue
211
+ model_id = str(item.get("model_id", item.get("model", "")))
212
+ if not model_id or model_id not in configured:
213
+ continue
214
+ raw_dimensions = item.get("dimensions", [])
215
+ dimensions: list[QuotaDimension] = []
216
+ if isinstance(raw_dimensions, list):
217
+ for raw in raw_dimensions:
218
+ if not isinstance(raw, dict) or not raw.get("name"):
219
+ continue
220
+ dimensions.append(
221
+ QuotaDimension(
222
+ name=str(raw["name"]),
223
+ limit=raw.get("limit"),
224
+ used=raw.get("used"),
225
+ remaining=raw.get("remaining"),
226
+ reset_at=_parse_datetime(raw.get("reset_at")),
227
+ window_seconds=_as_int(raw.get("window_seconds")),
228
+ confidence=QuotaConfidence.ACTUAL,
229
+ source="provider_discovery",
230
+ )
231
+ )
232
+ snapshots.append(
233
+ QuotaSnapshot(
234
+ model_id=model_id,
235
+ provider_id=provider.id,
236
+ observed_at=_now(),
237
+ dimensions=tuple(dimensions),
238
+ scope=str(item.get("scope", "model")),
239
+ metadata={"provider_metadata": item},
240
+ )
241
+ )
242
+ return snapshots
243
+
244
+ @staticmethod
245
+ def _extract(payload: dict[str, Any], value: object) -> object:
246
+ if value in (None, [], ""):
247
+ return None
248
+ if not isinstance(value, list):
249
+ return None
250
+ return _path(payload, tuple(str(item) for item in value))
251
+
252
+ @staticmethod
253
+ def _headers(discovery: dict[str, object]) -> dict[str, str]:
254
+ result = {str(k): str(v) for k, v in (discovery.get("headers", {}) or {}).items()}
255
+ prefixes = discovery.get("header_prefix", {}) or {}
256
+ for header, env_name in (discovery.get("header_env", {}) or {}).items():
257
+ if isinstance(env_name, str) and os.environ.get(env_name):
258
+ prefix = prefixes.get(header, "") if isinstance(prefixes, dict) else ""
259
+ result[str(header)] = f"{prefix}{os.environ[env_name]}"
260
+ return result
261
+
262
+ def _fetch(self, endpoint: str, headers: dict[str, str]) -> tuple[dict[str, Any], dict[str, str]]:
263
+ if not self.policy.permits("network"):
264
+ raise PermissionError("Network discovery is disabled by policy")
265
+ query_env = getattr(self, "_query_env", {})
266
+ if query_env:
267
+ from urllib.parse import urlencode, urlsplit, urlunsplit
268
+ parts = urlsplit(endpoint)
269
+ query = {key: os.environ.get(env, "") for key, env in query_env.items()}
270
+ endpoint = urlunsplit((parts.scheme, parts.netloc, parts.path, urlencode(query), parts.fragment))
271
+ request = urllib.request.Request(endpoint, headers=headers, method="GET")
272
+ try:
273
+ with urllib.request.urlopen(request, timeout=30) as response:
274
+ payload = json.loads(response.read().decode("utf-8"))
275
+ response_headers = {str(k).lower(): str(v) for k, v in response.headers.items()}
276
+ except HTTPError as exc:
277
+ raise RuntimeError(f"provider discovery failed with HTTP {exc.code}") from exc
278
+ except URLError as exc:
279
+ raise RuntimeError("provider discovery failed: network error") from exc
280
+ if not isinstance(payload, dict):
281
+ raise ValueError("provider discovery response must be a JSON object")
282
+ return payload, response_headers
283
+
284
+ def _normalize_capabilities(item: dict[str, object]) -> frozenset[str]:
285
+ """Normalize provider-specific metadata to VYRELON routing vocabulary."""
286
+ aliases = {
287
+ "generatecontent": "chat",
288
+ "generate_content": "chat",
289
+ "chat": "chat",
290
+ "embedcontent": "embeddings",
291
+ "embed_content": "embeddings",
292
+ "embeddings": "embeddings",
293
+ "predictlongrunning": "predict",
294
+ "counttokens": "token_count",
295
+ "function_calling": "tools",
296
+ "function-calling": "tools",
297
+ "structured-outputs": "structured_output",
298
+ }
299
+ result: set[str] = set()
300
+ for field in ("capabilities", "supportedGenerationMethods"):
301
+ for value in _string_list(item.get(field)):
302
+ key = value.strip().lower().replace(" ", "_")
303
+ result.add(aliases.get(key, key))
304
+ for field in ("input_modalities", "output_modalities", "modalities"):
305
+ for value in _string_list(item.get(field)):
306
+ key = value.strip().lower()
307
+ result.add(aliases.get(key, key))
308
+ for field, capability in (
309
+ ("supports_tools", "tools"),
310
+ ("supports_reasoning", "reasoning"),
311
+ ("supports_vision", "vision"),
312
+ ("supports_audio", "audio"),
313
+ ("supports_image", "image"),
314
+ ("supports_structured_output", "structured_output"),
315
+ ):
316
+ if item.get(field) is True:
317
+ result.add(capability)
318
+ return frozenset(result)
319
+
320
+
321
+ def _as_int(value: object) -> int | None:
322
+ try:
323
+ return int(value) if value is not None else None
324
+ except (TypeError, ValueError):
325
+ return None
326
+
327
+
328
+ def _parse_datetime(value: object) -> datetime | None:
329
+ if not value:
330
+ return None
331
+ try:
332
+ return datetime.fromisoformat(str(value).replace("Z", "+00:00"))
333
+ except ValueError:
334
+ return None
@@ -0,0 +1,71 @@
1
+ """Project-scoped execution configuration for VYRELON."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class ExecutionConfig:
12
+ version: int
13
+ runtime: str
14
+ agent_id: str
15
+ model_id: str
16
+
17
+ def validate(self) -> None:
18
+ if self.version != 1:
19
+ raise ValueError(f"unsupported execution config version: {self.version}")
20
+ if not self.runtime.strip():
21
+ raise ValueError("execution runtime must not be empty")
22
+ if not self.agent_id.strip():
23
+ raise ValueError("execution agent id must not be empty")
24
+ if not self.model_id.strip():
25
+ raise ValueError("execution model id must not be empty")
26
+
27
+ def to_dict(self) -> dict[str, object]:
28
+ self.validate()
29
+ return {
30
+ "version": self.version,
31
+ "runtime": self.runtime,
32
+ "agent_id": self.agent_id,
33
+ "model_id": self.model_id,
34
+ }
35
+
36
+
37
+ DEFAULT_EXECUTION_CONFIG = ExecutionConfig(
38
+ version=1,
39
+ runtime="process",
40
+ agent_id="cli-executor",
41
+ model_id="local-process",
42
+ )
43
+
44
+
45
+ def load_execution_config(project_root: Path) -> ExecutionConfig:
46
+ path = project_root / ".multiagentos" / "execution.json"
47
+ if not path.is_file():
48
+ raise FileNotFoundError(f"VYRELON execution config not found: {path}")
49
+ data = json.loads(path.read_text(encoding="utf-8"))
50
+ if not isinstance(data, dict):
51
+ raise ValueError("execution config must be a JSON object")
52
+ config = ExecutionConfig(
53
+ version=int(data.get("version", 0)),
54
+ runtime=str(data.get("runtime", "")),
55
+ agent_id=str(data.get("agent_id", "")),
56
+ model_id=str(data.get("model_id", "")),
57
+ )
58
+ config.validate()
59
+ return config
60
+
61
+
62
+ def write_default_execution_config(project_root: Path) -> Path:
63
+ target = project_root / ".multiagentos"
64
+ target.mkdir(parents=True, exist_ok=True)
65
+ path = target / "execution.json"
66
+ if not path.exists():
67
+ path.write_text(
68
+ json.dumps(DEFAULT_EXECUTION_CONFIG.to_dict(), indent=2) + "\n",
69
+ encoding="utf-8",
70
+ )
71
+ return path
@@ -0,0 +1,50 @@
1
+ """Built-in execution contracts used by the VYRELON CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from core.contracts import AgentContract, AIProvider, ModelSpec
6
+ from core.registry import AgentRegistry, AIRegistry
7
+
8
+
9
+ def default_execution_registries() -> tuple[AgentRegistry, AIRegistry]:
10
+ agents = AgentRegistry()
11
+ agents.register(
12
+ AgentContract(
13
+ id="cli-executor",
14
+ role="executor",
15
+ capabilities=frozenset({"process"}),
16
+ tools=frozenset({"process"}),
17
+ kind="runtime",
18
+ )
19
+ )
20
+
21
+ models = AIRegistry()
22
+ models.register(
23
+ AIProvider(
24
+ id="local",
25
+ kind="runtime",
26
+ models=(
27
+ ModelSpec(
28
+ id="local-process",
29
+ provider_id="local",
30
+ capabilities=frozenset({"process"}),
31
+ metadata={"runtime": "process"},
32
+ ),
33
+ ),
34
+ )
35
+ )
36
+ return agents, models
37
+
38
+
39
+ def resolve_execution_contracts(
40
+ agent_id: str,
41
+ model_id: str,
42
+ ) -> tuple[AgentContract, ModelSpec]:
43
+ agents, models = default_execution_registries()
44
+ agent = agents.get(agent_id)
45
+ model = models.model(model_id)
46
+ if not agent.capabilities.issubset(model.capabilities):
47
+ raise ValueError(
48
+ f"execution model {model.id!r} is incompatible with agent {agent.id!r}"
49
+ )
50
+ return agent, model
runtime/git.py ADDED
@@ -0,0 +1,84 @@
1
+ """Policy-controlled Git runtime for VYRELON."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from runtime.process import ProcessRuntime
6
+ from runtime.policy import ExecutionPolicy
7
+
8
+
9
+ class GitRuntime:
10
+ def __init__(self, process: ProcessRuntime | None = None, policy: ExecutionPolicy | None = None):
11
+ self.policy = policy or ExecutionPolicy()
12
+ self.process = process or ProcessRuntime(self.policy)
13
+
14
+ def status(self, cwd: str):
15
+ return self.process.run(["git", "status", "--short", "--branch"], cwd)
16
+
17
+ def diff(self, cwd: str, *, staged: bool = False, path: str | None = None):
18
+ command = ["git", "diff"]
19
+ if staged:
20
+ command.append("--staged")
21
+ if path:
22
+ command.extend(["--", path])
23
+ else:
24
+ command.append("--")
25
+ return self.process.run(command, cwd)
26
+
27
+ def log(self, cwd: str, count: int = 10):
28
+ return self.process.run(["git", "log", "--oneline", "-n", str(count)], cwd)
29
+
30
+ def add(self, cwd: str, paths: list[str] | None = None):
31
+ self._require_git_write()
32
+ command = ["git", "add"]
33
+ command.extend(paths if paths else ["-A"])
34
+ return self.process.run(command, cwd)
35
+
36
+ def checkout_branch(self, cwd: str, branch: str):
37
+ self._require_git_write()
38
+ return self.process.run(["git", "switch", branch], cwd)
39
+
40
+ def restore(self, cwd: str, paths: list[str], source: str = "HEAD"):
41
+ self._require_git_write()
42
+ return self.process.run(["git", "restore", "--source", source, "--", *paths], cwd)
43
+
44
+ def stash(self, cwd: str, action: str = "list", message: str | None = None):
45
+ command = ["git", "stash"]
46
+ if action == "list":
47
+ command.append("list")
48
+ else:
49
+ self._require_git_write()
50
+ command.append(action)
51
+ if action == "push" and message:
52
+ command.extend(["-m", message])
53
+ return self.process.run(command, cwd)
54
+
55
+ def commit(self, cwd: str, message: str, approved: bool = False):
56
+ self._require_git_write()
57
+ self._require_approval("git.commit", approved)
58
+ added = self.process.run(["git", "add", "-A"], cwd)
59
+ if added.returncode != 0:
60
+ return added
61
+ return self.process.run(["git", "commit", "-m", message], cwd)
62
+
63
+ def push(self, cwd: str, remote: str = "origin", branch: str | None = None, approved: bool = False):
64
+ self._require_git_write()
65
+ self._require_approval("git.push", approved)
66
+ command = ["git", "push", remote]
67
+ if branch:
68
+ command.append(branch)
69
+ return self.process.run(command, cwd)
70
+
71
+ def pull(self, cwd: str, remote: str = "origin", branch: str | None = None):
72
+ self._require_git_write()
73
+ command = ["git", "pull", remote]
74
+ if branch:
75
+ command.append(branch)
76
+ return self.process.run(command, cwd)
77
+
78
+ def _require_git_write(self):
79
+ if not self.policy.permits("git.write"):
80
+ raise PermissionError("Git write operations are disabled by policy")
81
+
82
+ def _require_approval(self, action: str, approved: bool):
83
+ if self.policy.requires_approval(action) and not approved:
84
+ raise PermissionError(f"Explicit approval required for: {action}")
runtime/github.py ADDED
@@ -0,0 +1,56 @@
1
+ """Policy-controlled GitHub runtime for VYRELON."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from core.contracts.github import GitHubGateway
6
+ from runtime.policy import ExecutionPolicy
7
+
8
+
9
+ class GitHubRuntime:
10
+ def __init__(self, gateway: GitHubGateway, policy: ExecutionPolicy | None = None) -> None:
11
+ self.gateway = gateway
12
+ self.policy = policy or ExecutionPolicy()
13
+
14
+ def _require_write(self) -> None:
15
+ if not self.policy.permits("github.write"):
16
+ raise PermissionError("GitHub write operations are disabled by policy")
17
+
18
+ def create_branch(self, repository: str, branch: str, base: str):
19
+ self._require_write()
20
+ return self.gateway.create_branch(repository, branch, base)
21
+
22
+ def create_file(self, repository: str, path: str, content: str, branch: str, message: str):
23
+ self._require_write()
24
+ return self.gateway.create_file(repository, path, content, branch, message)
25
+
26
+ def update_file(self, repository: str, path: str, content: str, sha: str, branch: str, message: str):
27
+ self._require_write()
28
+ return self.gateway.update_file(repository, path, content, sha, branch, message)
29
+
30
+ def create_issue(self, repository: str, title: str, body: str = ""):
31
+ self._require_write()
32
+ return self.gateway.create_issue(repository, title, body)
33
+
34
+ def create_pull_request(
35
+ self, repository: str, title: str, body: str, head: str, base: str,
36
+ draft: bool = False, approved: bool = False,
37
+ ):
38
+ self._require_write()
39
+ self._require_approval("github.pr", approved)
40
+ return self.gateway.create_pull_request(repository, title, body, head, base, draft)
41
+
42
+ def review_pull_request(self, repository: str, number: int, action: str, body: str = ""):
43
+ self._require_write()
44
+ return self.gateway.review_pull_request(repository, number, action, body)
45
+
46
+ def merge_pull_request(
47
+ self, repository: str, number: int, method: str = "squash",
48
+ approved: bool = False,
49
+ ):
50
+ self._require_write()
51
+ self._require_approval("github.merge", approved)
52
+ return self.gateway.merge_pull_request(repository, number, method)
53
+
54
+ def _require_approval(self, action: str, approved: bool) -> None:
55
+ if self.policy.requires_approval(action) and not approved:
56
+ raise PermissionError(f"Explicit approval required for: {action}")
@@ -0,0 +1,23 @@
1
+ """Probe VYRELON's local GitHub access."""
2
+
3
+ from integrations.github.gateway import GitHubGatewayClient
4
+
5
+
6
+ def probe(repository: str) -> dict:
7
+ client = GitHubGatewayClient()
8
+ repo = client.get_repository(repository)
9
+ branch = client.get_branch(repository, repo.default_branch)
10
+ return {
11
+ "repository": repo.full_name,
12
+ "private": repo.private,
13
+ "default_branch": repo.default_branch,
14
+ "default_branch_sha": branch.sha,
15
+ }
16
+
17
+
18
+ if __name__ == "__main__":
19
+ import argparse
20
+ parser = argparse.ArgumentParser()
21
+ parser.add_argument("repository")
22
+ args = parser.parse_args()
23
+ print(probe(args.repository))
runtime/health.py ADDED
@@ -0,0 +1,14 @@
1
+ """Runtime health registry facade."""
2
+ from core.contracts.health import (
3
+ HealthStatus,
4
+ ModelHealth,
5
+ ModelHealthRegistry,
6
+ ModelHealthStore,
7
+ )
8
+
9
+ __all__ = [
10
+ "HealthStatus",
11
+ "ModelHealth",
12
+ "ModelHealthRegistry",
13
+ "ModelHealthStore",
14
+ ]
@@ -0,0 +1,6 @@
1
+ """IDE adapter runtime."""
2
+
3
+ from runtime.ide.registry import IDEAdapterRegistry
4
+ from runtime.ide.runtime import IDERuntime
5
+
6
+ __all__ = ["IDEAdapterRegistry", "IDERuntime"]