resource-agent-system 0.5.7__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.
- generator/__init__.py +0 -0
- generator/agent_generator.py +106 -0
- generator/hashutil.py +9 -0
- generator/header.py +51 -0
- generator/model.py +94 -0
- generator/paths.py +17 -0
- generator/templates/Dockerfile.j2 +7 -0
- generator/templates/README.md.j2 +31 -0
- generator/templates/__init__.py.j2 +1 -0
- generator/templates/_dockerfile_header.j2 +1 -0
- generator/templates/_markdown_header.j2 +1 -0
- generator/templates/_python_header.j2 +1 -0
- generator/templates/agent_card.py.j2 +2 -0
- generator/templates/main.py.j2 +13 -0
- generator/templates/routes.py.j2 +87 -0
- generator/templates/test_contract.py.j2 +15 -0
- generator/validate.py +69 -0
- generator/verify.py +73 -0
- hypervisor/__init__.py +13 -0
- hypervisor/_version.py +20 -0
- hypervisor/cli.py +55 -0
- hypervisor/compatibility/__init__.py +0 -0
- hypervisor/compatibility/checker.py +43 -0
- hypervisor/config/__init__.py +24 -0
- hypervisor/config/defaults.py +63 -0
- hypervisor/config/env.py +54 -0
- hypervisor/config/loader.py +90 -0
- hypervisor/config/models.py +158 -0
- hypervisor/config/validators.py +57 -0
- hypervisor/contract_registry/__init__.py +0 -0
- hypervisor/contract_registry/cli.py +77 -0
- hypervisor/contract_registry/cross_validator.py +56 -0
- hypervisor/contract_registry/loader.py +80 -0
- hypervisor/contract_registry/merger.py +68 -0
- hypervisor/contract_registry/models.py +56 -0
- hypervisor/contract_registry/registry_builder.py +60 -0
- hypervisor/contract_registry/registry_exporter.py +29 -0
- hypervisor/contract_registry/schema_validator.py +54 -0
- hypervisor/contract_registry/validate.py +50 -0
- hypervisor/core.py +86 -0
- hypervisor/data/nlp2uri.yaml +50 -0
- hypervisor/deployment_registry/__init__.py +33 -0
- hypervisor/deployment_registry/loader.py +43 -0
- hypervisor/deployment_registry/models.py +47 -0
- hypervisor/deployment_registry/status.py +136 -0
- hypervisor/deployment_registry/writer.py +45 -0
- hypervisor/domain_pack/__init__.py +31 -0
- hypervisor/domain_pack/generator.py +272 -0
- hypervisor/domain_pack/templates.py +115 -0
- hypervisor/domain_pack/writer.py +11 -0
- hypervisor/evolution/__init__.py +0 -0
- hypervisor/evolution/cli.py +33 -0
- hypervisor/evolution/models.py +32 -0
- hypervisor/evolution/validator.py +16 -0
- hypervisor/paths.py +18 -0
- hypervisor/policy_gate/__init__.py +0 -0
- hypervisor/policy_gate/gate.py +26 -0
- hypervisor/py.typed +1 -0
- hypervisor/uri/__init__.py +0 -0
- hypervisor/uri/client.py +32 -0
- hypervisor/uri2llm/__init__.py +15 -0
- hypervisor/uri2llm/env_resolver.py +5 -0
- hypervisor/uri2llm/function_resolver.py +5 -0
- hypervisor/uri2llm/llm_resolver.py +5 -0
- hypervisor/uri2llm/protocol_resolver.py +10 -0
- hypervisor/uri2llm/pypi_resolver.py +5 -0
- hypervisor/uri2llm/router.py +5 -0
- hypervisor/verifier/__init__.py +0 -0
- hypervisor/verifier/capability_tests.py +32 -0
- hypervisor/verifier/cli.py +28 -0
- meta_agent/__init__.py +1 -0
- meta_agent/api.py +83 -0
- meta_agent/cli.py +93 -0
- meta_agent/domain_planner/__init__.py +1 -0
- meta_agent/domain_planner/domain_pack_generator.py +16 -0
- meta_agent/domain_planner/llm_planner.py +15 -0
- meta_agent/models.py +43 -0
- meta_agent/orchestrator.py +72 -0
- meta_agent/planner.py +159 -0
- meta_agent/repair/__init__.py +3 -0
- meta_agent/repair/loader.py +17 -0
- meta_agent/repair/pipeline.py +39 -0
- meta_agent/repair/rules.py +82 -0
- nl2a/__init__.py +0 -0
- nl2a/cli.py +25 -0
- nl2uri/__init__.py +0 -0
- nl2uri/cli.py +16 -0
- nl2uri/domain_planner.py +151 -0
- nl2uri/llm_planner.py +18 -0
- nl2uri/pipeline.py +95 -0
- nl2uri/planner.py +32 -0
- nl2uri/prompts/__init__.py +0 -0
- nl2uri/writer.py +7 -0
- resource_agent_system-0.5.7.dist-info/METADATA +189 -0
- resource_agent_system-0.5.7.dist-info/RECORD +129 -0
- resource_agent_system-0.5.7.dist-info/WHEEL +5 -0
- resource_agent_system-0.5.7.dist-info/entry_points.txt +5 -0
- resource_agent_system-0.5.7.dist-info/licenses/LICENSE +201 -0
- resource_agent_system-0.5.7.dist-info/top_level.txt +7 -0
- runtime_client/__init__.py +0 -0
- runtime_client/client.py +47 -0
- uri3/__init__.py +0 -0
- uri3/cli.py +46 -0
- uri3/discovery/__init__.py +0 -0
- uri3/graph/__init__.py +0 -0
- uri3/graph/uri_graph.py +51 -0
- uri3/logs/__init__.py +3 -0
- uri3/logs/reader.py +174 -0
- uri3/paths.py +17 -0
- uri3/protocols/__init__.py +0 -0
- uri3/protocols/normalizer.py +9 -0
- uri3/protocols/parser.py +17 -0
- uri3/protocols/schemes.py +4 -0
- uri3/resolvers/__init__.py +3 -0
- uri3/resolvers/env_resolver.py +21 -0
- uri3/resolvers/http_resolver.py +20 -0
- uri3/resolvers/llm_resolver.py +45 -0
- uri3/resolvers/log_resolver.py +126 -0
- uri3/resolvers/protocol_resolver.py +22 -0
- uri3/resolvers/pypi_resolver.py +16 -0
- uri3/resolvers/python_resolver.py +36 -0
- uri3/resolvers/router.py +99 -0
- uri3/scanner/__init__.py +0 -0
- uri3/scanner/base.py +7 -0
- uri3/scanner/http_scanner.py +16 -0
- uri3/scanner/scanner.py +36 -0
- uri3/validators/__init__.py +0 -0
- uri3/validators/uri_tree_validator.py +20 -0
- uri3/validators/uri_validator.py +9 -0
generator/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Iterable
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
from jinja2 import Environment, FileSystemLoader
|
|
9
|
+
|
|
10
|
+
from generator.hashutil import file_sha256
|
|
11
|
+
from generator.header import (
|
|
12
|
+
contract_source_ref,
|
|
13
|
+
dockerfile_header,
|
|
14
|
+
generated_marker_payload,
|
|
15
|
+
markdown_generated_banner,
|
|
16
|
+
python_file_header,
|
|
17
|
+
)
|
|
18
|
+
from generator.model import AgentSpec, load_agent_spec, spec_to_plain_dict
|
|
19
|
+
from generator.paths import find_repo_root
|
|
20
|
+
from generator.validate import validate_agent
|
|
21
|
+
|
|
22
|
+
PACKAGE_ROOT = Path(__file__).resolve().parent
|
|
23
|
+
ROOT = find_repo_root(Path(__file__))
|
|
24
|
+
TEMPLATES = PACKAGE_ROOT / "templates"
|
|
25
|
+
OUTPUT_ROOT = ROOT / "agents" / "generated"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def render_template(env: Environment, template_name: str, dest: Path, context: dict) -> None:
|
|
29
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
rendered = env.get_template(template_name).render(**context)
|
|
31
|
+
dest.write_text(rendered, encoding="utf-8")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def generate_agent(spec_path: Path, *, output_root: Path | None = None) -> Path:
|
|
35
|
+
errors = validate_agent(spec_path)
|
|
36
|
+
if errors:
|
|
37
|
+
raise ValueError("\n".join(errors))
|
|
38
|
+
|
|
39
|
+
spec: AgentSpec = load_agent_spec(spec_path)
|
|
40
|
+
contract_hash = file_sha256(spec_path)
|
|
41
|
+
source_ref = contract_source_ref(spec_path, ROOT)
|
|
42
|
+
base_output = output_root or OUTPUT_ROOT
|
|
43
|
+
output_dir = base_output / spec.output_dir_name
|
|
44
|
+
env = Environment(loader=FileSystemLoader(TEMPLATES), trim_blocks=True, lstrip_blocks=True)
|
|
45
|
+
|
|
46
|
+
context = {
|
|
47
|
+
"spec": spec,
|
|
48
|
+
"source_ref": source_ref,
|
|
49
|
+
"contract_hash": contract_hash,
|
|
50
|
+
"python_header": python_file_header(source_ref, contract_hash),
|
|
51
|
+
"dockerfile_header": dockerfile_header(source_ref, contract_hash),
|
|
52
|
+
"markdown_header": markdown_generated_banner(source_ref, contract_hash),
|
|
53
|
+
"agent_card": spec_to_plain_dict(spec, contract_hash, source_ref=source_ref),
|
|
54
|
+
"capability_names": [cap.name for cap in spec.capabilities],
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
files = {
|
|
58
|
+
"__init__.py.j2": output_dir / "__init__.py",
|
|
59
|
+
"main.py.j2": output_dir / "main.py",
|
|
60
|
+
"routes.py.j2": output_dir / "routes.py",
|
|
61
|
+
"agent_card.py.j2": output_dir / "agent_card.py",
|
|
62
|
+
"Dockerfile.j2": output_dir / "Dockerfile",
|
|
63
|
+
"README.md.j2": output_dir / "README.md",
|
|
64
|
+
"test_contract.py.j2": output_dir / "tests" / "test_contract.py",
|
|
65
|
+
}
|
|
66
|
+
for template, destination in files.items():
|
|
67
|
+
render_template(env, template, destination, context)
|
|
68
|
+
|
|
69
|
+
marker_path = output_dir / ".generated.yaml"
|
|
70
|
+
marker_path.write_text(
|
|
71
|
+
yaml.safe_dump(generated_marker_payload(source_ref, contract_hash), sort_keys=False),
|
|
72
|
+
encoding="utf-8",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return output_dir
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def expand_paths(patterns: Iterable[str]) -> list[Path]:
|
|
79
|
+
result: list[Path] = []
|
|
80
|
+
for pattern in patterns:
|
|
81
|
+
matches = sorted(Path().glob(pattern))
|
|
82
|
+
if matches:
|
|
83
|
+
result.extend(matches)
|
|
84
|
+
else:
|
|
85
|
+
path = Path(pattern)
|
|
86
|
+
if path.exists():
|
|
87
|
+
result.append(path)
|
|
88
|
+
return result
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def main(argv: list[str] | None = None) -> int:
|
|
92
|
+
argv = argv or sys.argv[1:]
|
|
93
|
+
if not argv:
|
|
94
|
+
argv = ["contracts/agents/*.yaml"]
|
|
95
|
+
paths = expand_paths(argv)
|
|
96
|
+
if not paths:
|
|
97
|
+
print("No agent specs matched.")
|
|
98
|
+
return 1
|
|
99
|
+
for path in paths:
|
|
100
|
+
output_dir = generate_agent(path)
|
|
101
|
+
print(f"Generated {output_dir}")
|
|
102
|
+
return 0
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
if __name__ == "__main__":
|
|
106
|
+
raise SystemExit(main())
|
generator/hashutil.py
ADDED
generator/header.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from generator.paths import project_root
|
|
6
|
+
|
|
7
|
+
AUTO_GENERATED_MARKER = "AUTO-GENERATED FILE. DO NOT EDIT."
|
|
8
|
+
GENERATOR_NAME = "resource-agent-factory"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def contract_source_ref(spec_path: Path, root: Path | None = None) -> str:
|
|
12
|
+
"""Return a stable repo-relative contract path for generated headers."""
|
|
13
|
+
base = (root or project_root()).resolve()
|
|
14
|
+
resolved = spec_path.expanduser().resolve()
|
|
15
|
+
try:
|
|
16
|
+
return resolved.relative_to(base).as_posix()
|
|
17
|
+
except ValueError:
|
|
18
|
+
return resolved.as_posix()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def python_file_header(source_ref: str, contract_hash: str) -> str:
|
|
22
|
+
return (
|
|
23
|
+
f"# {AUTO_GENERATED_MARKER}\n"
|
|
24
|
+
f"# Source: {source_ref}\n"
|
|
25
|
+
f"# Contract hash: {contract_hash}\n\n"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def dockerfile_header(source_ref: str, contract_hash: str) -> str:
|
|
30
|
+
return (
|
|
31
|
+
f"# {AUTO_GENERATED_MARKER}\n"
|
|
32
|
+
f"# Source: {source_ref}\n"
|
|
33
|
+
f"# Contract hash: {contract_hash}\n"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def markdown_generated_banner(source_ref: str, contract_hash: str) -> str:
|
|
38
|
+
return (
|
|
39
|
+
f"<!-- {AUTO_GENERATED_MARKER} -->\n"
|
|
40
|
+
f"<!-- Source: {source_ref} -->\n"
|
|
41
|
+
f"<!-- Contract hash: {contract_hash} -->\n"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def generated_marker_payload(source_ref: str, contract_hash: str) -> dict[str, str]:
|
|
46
|
+
return {
|
|
47
|
+
"auto_generated": "true",
|
|
48
|
+
"source": source_ref,
|
|
49
|
+
"contract_hash": contract_hash,
|
|
50
|
+
"generator": GENERATOR_NAME,
|
|
51
|
+
}
|
generator/model.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
CapabilityType = Literal["resource_read", "command"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class Capability:
|
|
14
|
+
name: str
|
|
15
|
+
type: CapabilityType
|
|
16
|
+
description: str = ""
|
|
17
|
+
uri: str | None = None
|
|
18
|
+
output_schema: str | None = None
|
|
19
|
+
renderer: str | None = None
|
|
20
|
+
command: str | None = None
|
|
21
|
+
input_schema: str | None = None
|
|
22
|
+
emits: list[str] = field(default_factory=list)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class AgentSpec:
|
|
27
|
+
source_path: Path
|
|
28
|
+
name: str
|
|
29
|
+
python_package: str
|
|
30
|
+
version: str
|
|
31
|
+
description: str
|
|
32
|
+
runtime_url_env: str
|
|
33
|
+
runtime_url_default: str
|
|
34
|
+
capabilities: list[Capability]
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def output_dir_name(self) -> str:
|
|
38
|
+
return self.python_package
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def load_agent_spec(path: str | Path) -> AgentSpec:
|
|
42
|
+
source_path = Path(path)
|
|
43
|
+
raw = yaml.safe_load(source_path.read_text(encoding="utf-8"))
|
|
44
|
+
if not isinstance(raw, dict):
|
|
45
|
+
raise ValueError(f"{source_path} must contain a YAML mapping")
|
|
46
|
+
|
|
47
|
+
agent = raw.get("agent") or {}
|
|
48
|
+
capabilities_raw = raw.get("capabilities") or []
|
|
49
|
+
|
|
50
|
+
capabilities: list[Capability] = []
|
|
51
|
+
for item in capabilities_raw:
|
|
52
|
+
capabilities.append(
|
|
53
|
+
Capability(
|
|
54
|
+
name=item["name"],
|
|
55
|
+
type=item["type"],
|
|
56
|
+
description=item.get("description", ""),
|
|
57
|
+
uri=item.get("uri"),
|
|
58
|
+
output_schema=item.get("output_schema"),
|
|
59
|
+
renderer=item.get("renderer"),
|
|
60
|
+
command=item.get("command"),
|
|
61
|
+
input_schema=item.get("input_schema"),
|
|
62
|
+
emits=item.get("emits", []) or [],
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
return AgentSpec(
|
|
67
|
+
source_path=source_path,
|
|
68
|
+
name=agent["name"],
|
|
69
|
+
python_package=agent.get("python_package") or agent["name"].replace("-", "_"),
|
|
70
|
+
version=str(agent.get("version", "0.1.0")),
|
|
71
|
+
description=agent.get("description", ""),
|
|
72
|
+
runtime_url_env=agent.get("runtime_url_env", "RESOURCE_RUNTIME_URL"),
|
|
73
|
+
runtime_url_default=agent.get("runtime_url_default", "http://localhost:8000"),
|
|
74
|
+
capabilities=capabilities,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def spec_to_plain_dict(
|
|
79
|
+
spec: AgentSpec,
|
|
80
|
+
contract_hash: str,
|
|
81
|
+
*,
|
|
82
|
+
source_ref: str | None = None,
|
|
83
|
+
) -> dict[str, Any]:
|
|
84
|
+
contract = source_ref or str(spec.source_path)
|
|
85
|
+
return {
|
|
86
|
+
"name": spec.name,
|
|
87
|
+
"version": spec.version,
|
|
88
|
+
"description": spec.description,
|
|
89
|
+
"generated_from": {
|
|
90
|
+
"contract": contract,
|
|
91
|
+
"contract_hash": contract_hash,
|
|
92
|
+
},
|
|
93
|
+
"capabilities": [cap.__dict__ for cap in spec.capabilities],
|
|
94
|
+
}
|
generator/paths.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def find_repo_root(start: Path | None = None) -> Path:
|
|
7
|
+
current = (start or Path(__file__)).resolve()
|
|
8
|
+
if current.is_file():
|
|
9
|
+
current = current.parent
|
|
10
|
+
for path in (current, *current.parents):
|
|
11
|
+
if (path / "contracts").is_dir() and (path / "schemas").is_dir():
|
|
12
|
+
return path
|
|
13
|
+
raise FileNotFoundError("Repository root not found (expected contracts/ and schemas/)")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def project_root() -> Path:
|
|
17
|
+
return find_repo_root()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{% include '_markdown_header.j2' %}
|
|
2
|
+
# {{ spec.name }}
|
|
3
|
+
|
|
4
|
+
Generated thin resource agent.
|
|
5
|
+
|
|
6
|
+
- Version: `{{ spec.version }}`
|
|
7
|
+
- Source: `{{ source_ref }}`
|
|
8
|
+
- Contract hash: `{{ contract_hash }}`
|
|
9
|
+
|
|
10
|
+
## Run
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
uvicorn agents.generated.{{ spec.python_package }}.main:app --reload --port 8101
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Endpoints
|
|
17
|
+
|
|
18
|
+
```txt
|
|
19
|
+
GET /health
|
|
20
|
+
GET /capabilities
|
|
21
|
+
GET /.well-known/agent.json
|
|
22
|
+
GET /.well-known/agent-card.json
|
|
23
|
+
GET /resources/read?uri=...
|
|
24
|
+
POST /commands
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Capabilities
|
|
28
|
+
|
|
29
|
+
{% for cap in spec.capabilities %}
|
|
30
|
+
- `{{ cap.name }}` — `{{ cap.type }}`{% if cap.uri %}, URI: `{{ cap.uri }}`{% endif %}{% if cap.command %}, command: `{{ cap.command }}`{% endif %}
|
|
31
|
+
{% endfor %}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{% include '_python_header.j2' %}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{{ dockerfile_header }}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{{ markdown_header }}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{{ python_header }}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{% include '_python_header.j2' %}
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from fastapi import FastAPI
|
|
5
|
+
|
|
6
|
+
from .routes import router
|
|
7
|
+
|
|
8
|
+
app = FastAPI(
|
|
9
|
+
title="{{ spec.name }}",
|
|
10
|
+
version="{{ spec.version }}",
|
|
11
|
+
description="{{ spec.description }}",
|
|
12
|
+
)
|
|
13
|
+
app.include_router(router)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{% include '_python_header.j2' %}
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, HTTPException, Query
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
from runtime_client.client import ResourceRuntimeClient
|
|
11
|
+
|
|
12
|
+
from .agent_card import AGENT_CARD
|
|
13
|
+
|
|
14
|
+
router = APIRouter()
|
|
15
|
+
|
|
16
|
+
RUNTIME_URL = os.getenv("{{ spec.runtime_url_env }}", "{{ spec.runtime_url_default }}")
|
|
17
|
+
client = ResourceRuntimeClient(base_url=RUNTIME_URL)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CommandRequest(BaseModel):
|
|
21
|
+
command: str = Field(..., description="Runtime command name")
|
|
22
|
+
payload: dict[str, Any] = Field(default_factory=dict)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@router.get("/health")
|
|
26
|
+
def health() -> dict[str, Any]:
|
|
27
|
+
return {
|
|
28
|
+
"ok": True,
|
|
29
|
+
"agent": "{{ spec.name }}",
|
|
30
|
+
"version": "{{ spec.version }}",
|
|
31
|
+
"runtime_url": RUNTIME_URL,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@router.get("/capabilities")
|
|
36
|
+
def capabilities() -> dict[str, Any]:
|
|
37
|
+
return {"capabilities": AGENT_CARD["capabilities"]}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@router.get("/.well-known/agent.json")
|
|
41
|
+
def well_known_agent_json() -> dict[str, Any]:
|
|
42
|
+
return AGENT_CARD
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@router.get("/.well-known/agent-card.json")
|
|
46
|
+
def well_known_agent_card_json() -> dict[str, Any]:
|
|
47
|
+
return AGENT_CARD
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@router.get("/resources/read")
|
|
51
|
+
def read_resource(uri: str = Query(...)) -> dict[str, Any]:
|
|
52
|
+
allowed = [cap.get("uri") for cap in AGENT_CARD["capabilities"] if cap.get("type") == "resource_read"]
|
|
53
|
+
if not _uri_allowed(uri, allowed):
|
|
54
|
+
raise HTTPException(status_code=403, detail=f"URI not exposed by this agent: {uri}")
|
|
55
|
+
return client.read_resource(uri)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@router.post("/commands")
|
|
59
|
+
def dispatch_command(request: CommandRequest) -> dict[str, Any]:
|
|
60
|
+
allowed = [cap.get("command") for cap in AGENT_CARD["capabilities"] if cap.get("type") == "command"]
|
|
61
|
+
if request.command not in allowed:
|
|
62
|
+
raise HTTPException(status_code=403, detail=f"Command not exposed by this agent: {request.command}")
|
|
63
|
+
return client.dispatch_command(request.command, request.payload)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
{% for cap in spec.capabilities if cap.type == "resource_read" %}
|
|
67
|
+
@router.get("/skills/{{ cap.name }}")
|
|
68
|
+
def skill_{{ cap.name }}({% for segment in cap.uri.split('{')[1:] %}{{ segment.split('}')[0] }}: str{% if not loop.last %}, {% endif %}{% endfor %}) -> dict[str, Any]:
|
|
69
|
+
uri = "{{ cap.uri }}"{% for segment in cap.uri.split('{')[1:] %}.replace("{{ '{' }}{{ segment.split('}')[0] }}{{ '}' }}", {{ segment.split('}')[0] }}){% endfor %}
|
|
70
|
+
return client.read_resource(uri)
|
|
71
|
+
{% endfor %}
|
|
72
|
+
|
|
73
|
+
{% for cap in spec.capabilities if cap.type == "command" %}
|
|
74
|
+
@router.post("/skills/{{ cap.name }}")
|
|
75
|
+
def skill_{{ cap.name }}(payload: dict[str, Any]) -> dict[str, Any]:
|
|
76
|
+
return client.dispatch_command("{{ cap.command }}", payload)
|
|
77
|
+
{% endfor %}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _uri_allowed(uri: str, templates: list[str | None]) -> bool:
|
|
81
|
+
for template in templates:
|
|
82
|
+
if not template:
|
|
83
|
+
continue
|
|
84
|
+
prefix = template.split("{")[0]
|
|
85
|
+
if uri.startswith(prefix):
|
|
86
|
+
return True
|
|
87
|
+
return False
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{% include '_python_header.j2' %}
|
|
2
|
+
from agents.generated.{{ spec.python_package }}.agent_card import AGENT_CARD
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_agent_card_has_expected_name():
|
|
6
|
+
assert AGENT_CARD["name"] == "{{ spec.name }}"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_agent_card_has_capabilities():
|
|
10
|
+
names = {cap["name"] for cap in AGENT_CARD["capabilities"]}
|
|
11
|
+
assert names == {{ capability_names | tojson }}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_agent_card_has_contract_hash():
|
|
15
|
+
assert AGENT_CARD["generated_from"]["contract_hash"] == "{{ contract_hash }}"
|
generator/validate.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from generator.model import load_agent_spec
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def validate_agent(path: Path) -> list[str]:
|
|
10
|
+
errors: list[str] = []
|
|
11
|
+
try:
|
|
12
|
+
spec = load_agent_spec(path)
|
|
13
|
+
except Exception as exc: # noqa: BLE001
|
|
14
|
+
return [f"{path}: cannot load spec: {exc}"]
|
|
15
|
+
|
|
16
|
+
names = set()
|
|
17
|
+
for cap in spec.capabilities:
|
|
18
|
+
if cap.name in names:
|
|
19
|
+
errors.append(f"{path}: duplicate capability name {cap.name}")
|
|
20
|
+
names.add(cap.name)
|
|
21
|
+
|
|
22
|
+
if cap.type == "resource_read":
|
|
23
|
+
if not cap.uri:
|
|
24
|
+
errors.append(f"{path}: {cap.name} resource_read requires uri")
|
|
25
|
+
if not cap.output_schema:
|
|
26
|
+
errors.append(f"{path}: {cap.name} resource_read requires output_schema")
|
|
27
|
+
if not cap.renderer:
|
|
28
|
+
errors.append(f"{path}: {cap.name} resource_read requires renderer")
|
|
29
|
+
elif cap.type == "command":
|
|
30
|
+
if not cap.command:
|
|
31
|
+
errors.append(f"{path}: {cap.name} command capability requires command")
|
|
32
|
+
if not cap.input_schema:
|
|
33
|
+
errors.append(f"{path}: {cap.name} command capability requires input_schema")
|
|
34
|
+
else:
|
|
35
|
+
errors.append(f"{path}: {cap.name} unsupported type {cap.type}")
|
|
36
|
+
|
|
37
|
+
return errors
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def iter_agent_specs(root: Path) -> list[Path]:
|
|
41
|
+
if root.is_file():
|
|
42
|
+
return [root]
|
|
43
|
+
return sorted((root / "agents").glob("*.yaml")) if (root / "agents").exists() else sorted(root.glob("*.yaml"))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def main(argv: list[str] | None = None) -> int:
|
|
47
|
+
argv = argv or sys.argv[1:]
|
|
48
|
+
root = Path(argv[0] if argv else "contracts")
|
|
49
|
+
paths = iter_agent_specs(root)
|
|
50
|
+
if not paths:
|
|
51
|
+
print(f"No agent specs found under {root}")
|
|
52
|
+
return 1
|
|
53
|
+
|
|
54
|
+
all_errors: list[str] = []
|
|
55
|
+
for path in paths:
|
|
56
|
+
all_errors.extend(validate_agent(path))
|
|
57
|
+
|
|
58
|
+
if all_errors:
|
|
59
|
+
print("Contract validation failed:")
|
|
60
|
+
for error in all_errors:
|
|
61
|
+
print(f"- {error}")
|
|
62
|
+
return 1
|
|
63
|
+
|
|
64
|
+
print(f"Validated {len(paths)} agent spec(s).")
|
|
65
|
+
return 0
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
raise SystemExit(main())
|
generator/verify.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from generator.hashutil import file_sha256
|
|
8
|
+
from generator.header import AUTO_GENERATED_MARKER
|
|
9
|
+
|
|
10
|
+
HASH_RE = re.compile(r"Contract hash: (sha256:[a-f0-9]{64})")
|
|
11
|
+
SOURCE_RE = re.compile(r"Source: ([^\n]+)")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def verify_generated_agent(agent_dir: Path) -> list[str]:
|
|
15
|
+
errors: list[str] = []
|
|
16
|
+
main_file = agent_dir / "main.py"
|
|
17
|
+
if not main_file.exists():
|
|
18
|
+
return [f"{agent_dir}: missing main.py"]
|
|
19
|
+
text = main_file.read_text(encoding="utf-8")
|
|
20
|
+
if AUTO_GENERATED_MARKER not in text:
|
|
21
|
+
return [f"{agent_dir}: missing auto-generated marker"]
|
|
22
|
+
source_match = SOURCE_RE.search(text)
|
|
23
|
+
hash_match = HASH_RE.search(text)
|
|
24
|
+
if not source_match or not hash_match:
|
|
25
|
+
return [f"{agent_dir}: missing generated source/hash header"]
|
|
26
|
+
|
|
27
|
+
source = Path(source_match.group(1))
|
|
28
|
+
expected_hash = hash_match.group(1)
|
|
29
|
+
if source.exists():
|
|
30
|
+
if file_sha256(source) != expected_hash:
|
|
31
|
+
errors.append(f"{agent_dir}: contract_hash mismatch; regenerate agent")
|
|
32
|
+
# else: source present and matches → OK
|
|
33
|
+
else:
|
|
34
|
+
# Source contract not present on this machine (e.g. generated on another host,
|
|
35
|
+
# or the agent dir is part of the committed examples). Do not fail verification
|
|
36
|
+
# for portability; only presence + hash match matters when the source is here.
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
return errors
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def verify_generated(root: Path) -> list[str]:
|
|
44
|
+
agent_dirs = [p for p in root.iterdir() if p.is_dir()] if root.exists() else []
|
|
45
|
+
errors: list[str] = []
|
|
46
|
+
if not agent_dirs:
|
|
47
|
+
return [f"No generated agents found in {root}"]
|
|
48
|
+
for agent_dir in agent_dirs:
|
|
49
|
+
errors.extend(verify_generated_agent(agent_dir))
|
|
50
|
+
return errors
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def main(argv: list[str] | None = None) -> int:
|
|
54
|
+
argv = argv or sys.argv[1:]
|
|
55
|
+
root = Path(argv[0] if argv else "agents/generated")
|
|
56
|
+
agent_dirs = [p for p in root.iterdir() if p.is_dir()] if root.exists() else []
|
|
57
|
+
if not agent_dirs:
|
|
58
|
+
print(f"No generated agents found in {root}")
|
|
59
|
+
return 1
|
|
60
|
+
|
|
61
|
+
errors = verify_generated(root)
|
|
62
|
+
|
|
63
|
+
if errors:
|
|
64
|
+
print("Generated verification failed:")
|
|
65
|
+
for error in errors:
|
|
66
|
+
print(f"- {error}")
|
|
67
|
+
return 1
|
|
68
|
+
print(f"Verified {len(agent_dirs)} generated agent(s).")
|
|
69
|
+
return 0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
raise SystemExit(main())
|
hypervisor/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""WronAI Hypervisor — public API surface."""
|
|
2
|
+
|
|
3
|
+
from ._version import __version__
|
|
4
|
+
from .config import get_config, load_config, get_default_config
|
|
5
|
+
from .core import Hypervisor
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"__version__",
|
|
9
|
+
"get_config",
|
|
10
|
+
"load_config",
|
|
11
|
+
"get_default_config",
|
|
12
|
+
"Hypervisor",
|
|
13
|
+
]
|
hypervisor/_version.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Single source of truth for package version at runtime.
|
|
3
|
+
|
|
4
|
+
Tries PEP 566 / importlib.metadata first (when installed),
|
|
5
|
+
falls back to a development default.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
__all__ = ["__version__"]
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
__version__ = version("hypervisor")
|
|
17
|
+
except PackageNotFoundError:
|
|
18
|
+
__version__ = "0.1.0.dev0"
|
|
19
|
+
except Exception: # pragma: no cover
|
|
20
|
+
__version__ = "0.1.0.dev0"
|