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.
Files changed (129) hide show
  1. generator/__init__.py +0 -0
  2. generator/agent_generator.py +106 -0
  3. generator/hashutil.py +9 -0
  4. generator/header.py +51 -0
  5. generator/model.py +94 -0
  6. generator/paths.py +17 -0
  7. generator/templates/Dockerfile.j2 +7 -0
  8. generator/templates/README.md.j2 +31 -0
  9. generator/templates/__init__.py.j2 +1 -0
  10. generator/templates/_dockerfile_header.j2 +1 -0
  11. generator/templates/_markdown_header.j2 +1 -0
  12. generator/templates/_python_header.j2 +1 -0
  13. generator/templates/agent_card.py.j2 +2 -0
  14. generator/templates/main.py.j2 +13 -0
  15. generator/templates/routes.py.j2 +87 -0
  16. generator/templates/test_contract.py.j2 +15 -0
  17. generator/validate.py +69 -0
  18. generator/verify.py +73 -0
  19. hypervisor/__init__.py +13 -0
  20. hypervisor/_version.py +20 -0
  21. hypervisor/cli.py +55 -0
  22. hypervisor/compatibility/__init__.py +0 -0
  23. hypervisor/compatibility/checker.py +43 -0
  24. hypervisor/config/__init__.py +24 -0
  25. hypervisor/config/defaults.py +63 -0
  26. hypervisor/config/env.py +54 -0
  27. hypervisor/config/loader.py +90 -0
  28. hypervisor/config/models.py +158 -0
  29. hypervisor/config/validators.py +57 -0
  30. hypervisor/contract_registry/__init__.py +0 -0
  31. hypervisor/contract_registry/cli.py +77 -0
  32. hypervisor/contract_registry/cross_validator.py +56 -0
  33. hypervisor/contract_registry/loader.py +80 -0
  34. hypervisor/contract_registry/merger.py +68 -0
  35. hypervisor/contract_registry/models.py +56 -0
  36. hypervisor/contract_registry/registry_builder.py +60 -0
  37. hypervisor/contract_registry/registry_exporter.py +29 -0
  38. hypervisor/contract_registry/schema_validator.py +54 -0
  39. hypervisor/contract_registry/validate.py +50 -0
  40. hypervisor/core.py +86 -0
  41. hypervisor/data/nlp2uri.yaml +50 -0
  42. hypervisor/deployment_registry/__init__.py +33 -0
  43. hypervisor/deployment_registry/loader.py +43 -0
  44. hypervisor/deployment_registry/models.py +47 -0
  45. hypervisor/deployment_registry/status.py +136 -0
  46. hypervisor/deployment_registry/writer.py +45 -0
  47. hypervisor/domain_pack/__init__.py +31 -0
  48. hypervisor/domain_pack/generator.py +272 -0
  49. hypervisor/domain_pack/templates.py +115 -0
  50. hypervisor/domain_pack/writer.py +11 -0
  51. hypervisor/evolution/__init__.py +0 -0
  52. hypervisor/evolution/cli.py +33 -0
  53. hypervisor/evolution/models.py +32 -0
  54. hypervisor/evolution/validator.py +16 -0
  55. hypervisor/paths.py +18 -0
  56. hypervisor/policy_gate/__init__.py +0 -0
  57. hypervisor/policy_gate/gate.py +26 -0
  58. hypervisor/py.typed +1 -0
  59. hypervisor/uri/__init__.py +0 -0
  60. hypervisor/uri/client.py +32 -0
  61. hypervisor/uri2llm/__init__.py +15 -0
  62. hypervisor/uri2llm/env_resolver.py +5 -0
  63. hypervisor/uri2llm/function_resolver.py +5 -0
  64. hypervisor/uri2llm/llm_resolver.py +5 -0
  65. hypervisor/uri2llm/protocol_resolver.py +10 -0
  66. hypervisor/uri2llm/pypi_resolver.py +5 -0
  67. hypervisor/uri2llm/router.py +5 -0
  68. hypervisor/verifier/__init__.py +0 -0
  69. hypervisor/verifier/capability_tests.py +32 -0
  70. hypervisor/verifier/cli.py +28 -0
  71. meta_agent/__init__.py +1 -0
  72. meta_agent/api.py +83 -0
  73. meta_agent/cli.py +93 -0
  74. meta_agent/domain_planner/__init__.py +1 -0
  75. meta_agent/domain_planner/domain_pack_generator.py +16 -0
  76. meta_agent/domain_planner/llm_planner.py +15 -0
  77. meta_agent/models.py +43 -0
  78. meta_agent/orchestrator.py +72 -0
  79. meta_agent/planner.py +159 -0
  80. meta_agent/repair/__init__.py +3 -0
  81. meta_agent/repair/loader.py +17 -0
  82. meta_agent/repair/pipeline.py +39 -0
  83. meta_agent/repair/rules.py +82 -0
  84. nl2a/__init__.py +0 -0
  85. nl2a/cli.py +25 -0
  86. nl2uri/__init__.py +0 -0
  87. nl2uri/cli.py +16 -0
  88. nl2uri/domain_planner.py +151 -0
  89. nl2uri/llm_planner.py +18 -0
  90. nl2uri/pipeline.py +95 -0
  91. nl2uri/planner.py +32 -0
  92. nl2uri/prompts/__init__.py +0 -0
  93. nl2uri/writer.py +7 -0
  94. resource_agent_system-0.5.7.dist-info/METADATA +189 -0
  95. resource_agent_system-0.5.7.dist-info/RECORD +129 -0
  96. resource_agent_system-0.5.7.dist-info/WHEEL +5 -0
  97. resource_agent_system-0.5.7.dist-info/entry_points.txt +5 -0
  98. resource_agent_system-0.5.7.dist-info/licenses/LICENSE +201 -0
  99. resource_agent_system-0.5.7.dist-info/top_level.txt +7 -0
  100. runtime_client/__init__.py +0 -0
  101. runtime_client/client.py +47 -0
  102. uri3/__init__.py +0 -0
  103. uri3/cli.py +46 -0
  104. uri3/discovery/__init__.py +0 -0
  105. uri3/graph/__init__.py +0 -0
  106. uri3/graph/uri_graph.py +51 -0
  107. uri3/logs/__init__.py +3 -0
  108. uri3/logs/reader.py +174 -0
  109. uri3/paths.py +17 -0
  110. uri3/protocols/__init__.py +0 -0
  111. uri3/protocols/normalizer.py +9 -0
  112. uri3/protocols/parser.py +17 -0
  113. uri3/protocols/schemes.py +4 -0
  114. uri3/resolvers/__init__.py +3 -0
  115. uri3/resolvers/env_resolver.py +21 -0
  116. uri3/resolvers/http_resolver.py +20 -0
  117. uri3/resolvers/llm_resolver.py +45 -0
  118. uri3/resolvers/log_resolver.py +126 -0
  119. uri3/resolvers/protocol_resolver.py +22 -0
  120. uri3/resolvers/pypi_resolver.py +16 -0
  121. uri3/resolvers/python_resolver.py +36 -0
  122. uri3/resolvers/router.py +99 -0
  123. uri3/scanner/__init__.py +0 -0
  124. uri3/scanner/base.py +7 -0
  125. uri3/scanner/http_scanner.py +16 -0
  126. uri3/scanner/scanner.py +36 -0
  127. uri3/validators/__init__.py +0 -0
  128. uri3/validators/uri_tree_validator.py +20 -0
  129. 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
@@ -0,0 +1,9 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ from pathlib import Path
5
+
6
+
7
+ def file_sha256(path: str | Path) -> str:
8
+ data = Path(path).read_bytes()
9
+ return "sha256:" + hashlib.sha256(data).hexdigest()
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,7 @@
1
+ {% include '_dockerfile_header.j2' %}
2
+ FROM python:3.12-slim
3
+ WORKDIR /app
4
+ COPY . /app
5
+ RUN pip install --no-cache-dir -e .
6
+ EXPOSE 8101
7
+ CMD ["uvicorn", "agents.generated.{{ spec.python_package }}.main:app", "--host", "0.0.0.0", "--port", "8101"]
@@ -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,2 @@
1
+ {% include '_python_header.j2' %}
2
+ AGENT_CARD = {{ agent_card | tojson(indent=2) }}
@@ -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"