agent-operating-substrate 0.1.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.
- agent_operating_substrate-0.1.0.dist-info/METADATA +344 -0
- agent_operating_substrate-0.1.0.dist-info/RECORD +29 -0
- agent_operating_substrate-0.1.0.dist-info/WHEEL +5 -0
- agent_operating_substrate-0.1.0.dist-info/entry_points.txt +2 -0
- agent_operating_substrate-0.1.0.dist-info/top_level.txt +1 -0
- aos/__init__.py +65 -0
- aos/__main__.py +7 -0
- aos/autopsy.py +177 -0
- aos/blackboard.py +114 -0
- aos/ci.py +201 -0
- aos/cli.py +772 -0
- aos/curator.py +143 -0
- aos/daemon.py +121 -0
- aos/discovery.py +72 -0
- aos/enforcer.py +374 -0
- aos/engine.py +110 -0
- aos/executor.py +80 -0
- aos/fleet.py +160 -0
- aos/harness.py +151 -0
- aos/hook.py +112 -0
- aos/identity.py +205 -0
- aos/ingest.py +251 -0
- aos/matcher.py +125 -0
- aos/mcp.py +188 -0
- aos/mesh.py +155 -0
- aos/models.py +107 -0
- aos/packs.py +408 -0
- aos/ui.py +3205 -0
- aos/validator.py +123 -0
aos/ingest.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""Automated repository ingestion and convention extraction engine."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import re
|
|
9
|
+
import time
|
|
10
|
+
from typing import Any
|
|
11
|
+
import yaml
|
|
12
|
+
|
|
13
|
+
from aos.autopsy import inscribe_candidate, promote_candidate, synthesize_candidate_rule
|
|
14
|
+
from aos.blackboard import post_event
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class IngestionReport:
|
|
19
|
+
detected_languages: list[str] = field(default_factory=list)
|
|
20
|
+
detected_frameworks: list[str] = field(default_factory=list)
|
|
21
|
+
scanned_config_files: list[str] = field(default_factory=list)
|
|
22
|
+
existing_instruction_files: list[str] = field(default_factory=list)
|
|
23
|
+
synthesized_rules: list[dict[str, Any]] = field(default_factory=list)
|
|
24
|
+
recommended_packs: list[str] = field(default_factory=list)
|
|
25
|
+
|
|
26
|
+
def to_dict(self) -> dict[str, Any]:
|
|
27
|
+
return {
|
|
28
|
+
"detected_languages": self.detected_languages,
|
|
29
|
+
"detected_frameworks": self.detected_frameworks,
|
|
30
|
+
"scanned_config_files": self.scanned_config_files,
|
|
31
|
+
"existing_instruction_files": self.existing_instruction_files,
|
|
32
|
+
"recommended_packs": self.recommended_packs,
|
|
33
|
+
"synthesized_rules_count": len(self.synthesized_rules),
|
|
34
|
+
"synthesized_rules": self.synthesized_rules,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def scan_repository_conventions(root_dir: Path | str = ".") -> IngestionReport:
|
|
39
|
+
"""Inspect repository manifests and instruction files to derive tailored invariants."""
|
|
40
|
+
root = Path(root_dir)
|
|
41
|
+
report = IngestionReport()
|
|
42
|
+
detected_rules: list[dict[str, Any]] = []
|
|
43
|
+
|
|
44
|
+
# 1. Check TypeScript / JavaScript ecosystems
|
|
45
|
+
tsconfig = root / "tsconfig.json"
|
|
46
|
+
if tsconfig.is_file():
|
|
47
|
+
report.detected_languages.append("typescript")
|
|
48
|
+
report.scanned_config_files.append("tsconfig.json")
|
|
49
|
+
try:
|
|
50
|
+
ts_text = tsconfig.read_text(encoding="utf-8", errors="ignore")
|
|
51
|
+
clean_ts = re.sub(r"//.*", "", ts_text)
|
|
52
|
+
ts_data = json.loads(clean_ts)
|
|
53
|
+
co = ts_data.get("compilerOptions", {})
|
|
54
|
+
if co.get("strict", False) or co.get("noImplicitAny", False):
|
|
55
|
+
detected_rules.append({
|
|
56
|
+
"id": "ts-strict-typing",
|
|
57
|
+
"statement": "TypeScript code must maintain strict type safety: avoid explicit any casts or unvalidated dynamic properties.",
|
|
58
|
+
"rationale": "Enforce strict compiler options declared in tsconfig.json across all AI edits.",
|
|
59
|
+
"paths": ["src/**/*.ts", "src/**/*.tsx"],
|
|
60
|
+
"languages": ["typescript"],
|
|
61
|
+
"enforcement": "reject_diff",
|
|
62
|
+
})
|
|
63
|
+
except Exception:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
pkg_json = root / "package.json"
|
|
67
|
+
if pkg_json.is_file():
|
|
68
|
+
report.scanned_config_files.append("package.json")
|
|
69
|
+
try:
|
|
70
|
+
pkg_data = json.loads(pkg_json.read_text(encoding="utf-8", errors="ignore"))
|
|
71
|
+
deps = {**pkg_data.get("dependencies", {}), **pkg_data.get("devDependencies", {})}
|
|
72
|
+
if "react" in deps:
|
|
73
|
+
report.detected_frameworks.append("react")
|
|
74
|
+
if "javascript" not in report.detected_languages:
|
|
75
|
+
report.detected_languages.append("javascript")
|
|
76
|
+
detected_rules.append({
|
|
77
|
+
"id": "react-hooks-deps",
|
|
78
|
+
"statement": "React components must obey the rules of hooks and specify complete dependency arrays in useEffect and useMemo.",
|
|
79
|
+
"rationale": "Prevents stale closure bugs and infinite re-render cycles in React components.",
|
|
80
|
+
"paths": ["src/**/*.jsx", "src/**/*.tsx"],
|
|
81
|
+
"languages": ["javascript", "typescript"],
|
|
82
|
+
"enforcement": "reject_diff",
|
|
83
|
+
})
|
|
84
|
+
if "next" in deps:
|
|
85
|
+
report.detected_frameworks.append("nextjs")
|
|
86
|
+
detected_rules.append({
|
|
87
|
+
"id": "nextjs-client-boundary",
|
|
88
|
+
"statement": "Interactive components utilizing state or browser hooks must explicitly declare the 'use client' directive.",
|
|
89
|
+
"rationale": "Next.js App Router enforces strict server and client component boundaries.",
|
|
90
|
+
"paths": ["src/app/**/*.tsx", "src/components/**/*.tsx"],
|
|
91
|
+
"languages": ["typescript", "javascript"],
|
|
92
|
+
"enforcement": "reject_diff",
|
|
93
|
+
})
|
|
94
|
+
except Exception:
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
# 2. Check Python ecosystems
|
|
98
|
+
pyproject = root / "pyproject.toml"
|
|
99
|
+
reqs = root / "requirements.txt"
|
|
100
|
+
is_python = False
|
|
101
|
+
py_text = ""
|
|
102
|
+
|
|
103
|
+
if pyproject.is_file():
|
|
104
|
+
is_python = True
|
|
105
|
+
report.scanned_config_files.append("pyproject.toml")
|
|
106
|
+
py_text += pyproject.read_text(encoding="utf-8", errors="ignore")
|
|
107
|
+
|
|
108
|
+
if reqs.is_file():
|
|
109
|
+
is_python = True
|
|
110
|
+
report.scanned_config_files.append("requirements.txt")
|
|
111
|
+
py_text += reqs.read_text(encoding="utf-8", errors="ignore")
|
|
112
|
+
|
|
113
|
+
if is_python:
|
|
114
|
+
report.detected_languages.append("python")
|
|
115
|
+
if "fastapi" in py_text.lower():
|
|
116
|
+
report.detected_frameworks.append("fastapi")
|
|
117
|
+
detected_rules.append({
|
|
118
|
+
"id": "fastapi-pydantic-schemas",
|
|
119
|
+
"statement": "FastAPI route handlers must define explicit Pydantic request and response models for typed I/O contracts.",
|
|
120
|
+
"rationale": "Ensures runtime schema validation and automatic OpenAPI documentation generation.",
|
|
121
|
+
"paths": ["src/**/*.py", "app/**/*.py"],
|
|
122
|
+
"languages": ["python"],
|
|
123
|
+
"enforcement": "reject_diff",
|
|
124
|
+
})
|
|
125
|
+
if "django" in py_text.lower():
|
|
126
|
+
report.detected_frameworks.append("django")
|
|
127
|
+
detected_rules.append({
|
|
128
|
+
"id": "django-atomic-writes",
|
|
129
|
+
"statement": "Mutating database operations across multiple models must execute within transaction.atomic() blocks.",
|
|
130
|
+
"rationale": "Guarantees database consistency and prevents partial write corruptions.",
|
|
131
|
+
"paths": ["src/**/*.py"],
|
|
132
|
+
"languages": ["python"],
|
|
133
|
+
"enforcement": "reject_diff",
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
# 3. Check Rust
|
|
137
|
+
cargo = root / "Cargo.toml"
|
|
138
|
+
if cargo.is_file():
|
|
139
|
+
report.detected_languages.append("rust")
|
|
140
|
+
report.scanned_config_files.append("Cargo.toml")
|
|
141
|
+
detected_rules.append({
|
|
142
|
+
"id": "rust-unsafe-audit",
|
|
143
|
+
"statement": "Unsafe blocks in Rust must include a preceding '// SAFETY:' comment explaining pointer or memory validity invariants.",
|
|
144
|
+
"rationale": "Audit requirement ensuring memory safety invariants are documented at each unsafe boundary.",
|
|
145
|
+
"paths": ["src/**/*.rs"],
|
|
146
|
+
"languages": ["rust"],
|
|
147
|
+
"enforcement": "reject_diff",
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
# 4. Check Go
|
|
151
|
+
gomod = root / "go.mod"
|
|
152
|
+
if gomod.is_file():
|
|
153
|
+
report.detected_languages.append("go")
|
|
154
|
+
report.scanned_config_files.append("go.mod")
|
|
155
|
+
detected_rules.append({
|
|
156
|
+
"id": "go-error-wrapping",
|
|
157
|
+
"statement": "Go functions must explicitly check and wrap returned errors using fmt.Errorf with %w.",
|
|
158
|
+
"rationale": "Preserves error call chains and enables errors.Is / errors.As inspection in Go services.",
|
|
159
|
+
"paths": ["**/*.go"],
|
|
160
|
+
"languages": ["go"],
|
|
161
|
+
"enforcement": "reject_diff",
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
# 5. Scan Existing Team Instruction Files
|
|
165
|
+
instruction_candidates = [
|
|
166
|
+
"CLAUDE.md",
|
|
167
|
+
"AGENTS.md",
|
|
168
|
+
".cursorrules",
|
|
169
|
+
"CONTRIBUTING.md",
|
|
170
|
+
".github/copilot-instructions.md",
|
|
171
|
+
]
|
|
172
|
+
for instr_name in instruction_candidates:
|
|
173
|
+
instr_path = root / instr_name
|
|
174
|
+
if instr_path.is_file():
|
|
175
|
+
report.existing_instruction_files.append(instr_name)
|
|
176
|
+
try:
|
|
177
|
+
instr_text = instr_path.read_text(encoding="utf-8", errors="ignore")
|
|
178
|
+
clean_instr = re.sub(r"<!-- AOS_INVARIANTS_START -->.*?<!-- AOS_INVARIANTS_END -->", "", instr_text, flags=re.DOTALL)
|
|
179
|
+
rule_lines = re.findall(r"^(?:[-*]|\d+\.)\s+([A-Z][^\n]{15,120})", clean_instr, re.MULTILINE)
|
|
180
|
+
for idx, r_stmt in enumerate(rule_lines[:3], 1):
|
|
181
|
+
safe_id = re.sub(r"[^a-z0-9]+", "-", instr_name.lower().replace(".", "-")).strip("-")
|
|
182
|
+
r_id = f"team-{safe_id}-rule-{idx}"
|
|
183
|
+
clean_stmt = r_stmt.strip()
|
|
184
|
+
if not clean_stmt.endswith("."):
|
|
185
|
+
clean_stmt += "."
|
|
186
|
+
if not any(d["statement"] == clean_stmt for d in detected_rules):
|
|
187
|
+
detected_rules.append({
|
|
188
|
+
"id": r_id,
|
|
189
|
+
"statement": clean_stmt,
|
|
190
|
+
"rationale": f"Extracted from team repository instructions in {instr_name}.",
|
|
191
|
+
"paths": ["**/*"],
|
|
192
|
+
"languages": [],
|
|
193
|
+
"enforcement": "reject_diff",
|
|
194
|
+
})
|
|
195
|
+
except Exception:
|
|
196
|
+
pass
|
|
197
|
+
|
|
198
|
+
from aos.packs import recommend_packs
|
|
199
|
+
report.recommended_packs = recommend_packs(root_dir=root)
|
|
200
|
+
report.synthesized_rules = detected_rules
|
|
201
|
+
return report
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def ingest_repository(
|
|
205
|
+
root_dir: Path | str = ".",
|
|
206
|
+
auto_promote: bool = False,
|
|
207
|
+
install_recommended_packs: bool = False,
|
|
208
|
+
) -> IngestionReport:
|
|
209
|
+
"""Scan repository conventions and inscribe synthesized invariants into the substrate."""
|
|
210
|
+
root = Path(root_dir)
|
|
211
|
+
report = scan_repository_conventions(root_dir=root)
|
|
212
|
+
sub_dir = root / ".agents" / "substrate"
|
|
213
|
+
|
|
214
|
+
for rule_data in report.synthesized_rules:
|
|
215
|
+
candidate = synthesize_candidate_rule(
|
|
216
|
+
rule_id=rule_data["id"],
|
|
217
|
+
statement=rule_data["statement"],
|
|
218
|
+
rationale=rule_data.get("rationale", "Synthesized via repository ingestion scan."),
|
|
219
|
+
paths=rule_data.get("paths", ["**/*"]),
|
|
220
|
+
languages=rule_data.get("languages", []),
|
|
221
|
+
enforcement=rule_data.get("enforcement", "reject_diff"),
|
|
222
|
+
incident_id=f"ingest-{int(time.time())}",
|
|
223
|
+
inscribing_agent="repo-ingestion-engine",
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
inscribe_candidate(candidate, substrate_dir=sub_dir)
|
|
227
|
+
if auto_promote:
|
|
228
|
+
promote_candidate(candidate.id, peer_agent="repo-ingestion-engine", substrate_dir=sub_dir)
|
|
229
|
+
|
|
230
|
+
if install_recommended_packs:
|
|
231
|
+
from aos.packs import install_pack
|
|
232
|
+
for p in report.recommended_packs:
|
|
233
|
+
install_pack(p, root_dir=root, promote=auto_promote)
|
|
234
|
+
|
|
235
|
+
post_event(
|
|
236
|
+
{
|
|
237
|
+
"type": "REPO_INGESTED",
|
|
238
|
+
"sender": "repo-ingestion-engine",
|
|
239
|
+
"payload": {
|
|
240
|
+
"detected_languages": report.detected_languages,
|
|
241
|
+
"detected_frameworks": report.detected_frameworks,
|
|
242
|
+
"scanned_config_files": report.scanned_config_files,
|
|
243
|
+
"existing_instruction_files": report.existing_instruction_files,
|
|
244
|
+
"synthesized_count": len(report.synthesized_rules),
|
|
245
|
+
"auto_promoted": auto_promote,
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
root_dir=root,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
return report
|
aos/matcher.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Matching engine for invariant rule scopes (paths and languages)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import glob
|
|
5
|
+
import re
|
|
6
|
+
from functools import lru_cache
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from aos.models import InvariantRule
|
|
11
|
+
|
|
12
|
+
EXTENSION_LANGUAGE_MAP: dict[str, str] = {
|
|
13
|
+
".py": "python",
|
|
14
|
+
".cpp": "cpp",
|
|
15
|
+
".cc": "cpp",
|
|
16
|
+
".cxx": "cpp",
|
|
17
|
+
".hpp": "cpp",
|
|
18
|
+
".hxx": "cpp",
|
|
19
|
+
".h": "cpp",
|
|
20
|
+
".c": "c",
|
|
21
|
+
".cu": "cuda",
|
|
22
|
+
".cuh": "cuda",
|
|
23
|
+
".rs": "rust",
|
|
24
|
+
".go": "go",
|
|
25
|
+
".js": "javascript",
|
|
26
|
+
".mjs": "javascript",
|
|
27
|
+
".cjs": "javascript",
|
|
28
|
+
".ts": "typescript",
|
|
29
|
+
".mts": "typescript",
|
|
30
|
+
".cts": "typescript",
|
|
31
|
+
".java": "java",
|
|
32
|
+
".yaml": "yaml",
|
|
33
|
+
".yml": "yaml",
|
|
34
|
+
".json": "json",
|
|
35
|
+
".sh": "bash",
|
|
36
|
+
".bash": "bash",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@lru_cache(maxsize=4096)
|
|
41
|
+
def _compile_glob(pattern: str) -> re.Pattern[str]:
|
|
42
|
+
# Normalize slashes in pattern
|
|
43
|
+
normalized_pattern = pattern.replace("\\", "/")
|
|
44
|
+
regex_str = glob.translate(normalized_pattern, recursive=True, include_hidden=True)
|
|
45
|
+
return re.compile(regex_str)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@lru_cache(maxsize=4096)
|
|
49
|
+
def normalize_path(path: str) -> str:
|
|
50
|
+
"""Normalize file path to POSIX style without leading relative prefixes."""
|
|
51
|
+
p = path.replace("\\", "/")
|
|
52
|
+
if p.startswith("./"):
|
|
53
|
+
p = p[2:]
|
|
54
|
+
return p
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def match_path(pattern: str, target_path: str) -> bool:
|
|
58
|
+
"""Match a target path against a glob pattern with globstar (**) support."""
|
|
59
|
+
normalized = normalize_path(target_path)
|
|
60
|
+
regex = _compile_glob(pattern)
|
|
61
|
+
return bool(regex.match(normalized))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@lru_cache(maxsize=4096)
|
|
65
|
+
def infer_language(file_path: str) -> Optional[str]:
|
|
66
|
+
"""Infer the programming language identifier from a file path extension."""
|
|
67
|
+
dot_idx = file_path.rfind(".")
|
|
68
|
+
if dot_idx == -1:
|
|
69
|
+
return None
|
|
70
|
+
slash_idx = max(file_path.rfind("/"), file_path.rfind("\\"))
|
|
71
|
+
if dot_idx <= slash_idx:
|
|
72
|
+
return None
|
|
73
|
+
suffix = file_path[dot_idx:].lower()
|
|
74
|
+
return EXTENSION_LANGUAGE_MAP.get(suffix)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@lru_cache(maxsize=4096)
|
|
78
|
+
def extract_path_prefix(pattern: str) -> str:
|
|
79
|
+
"""Extract literal directory prefix before any glob wildcard character."""
|
|
80
|
+
norm = normalize_path(pattern)
|
|
81
|
+
wildcard_indices = [norm.find(ch) for ch in ("*", "?", "[") if norm.find(ch) != -1]
|
|
82
|
+
limit = min(wildcard_indices) if wildcard_indices else len(norm)
|
|
83
|
+
literal_prefix = norm[:limit]
|
|
84
|
+
slash_idx = literal_prefix.rfind("/")
|
|
85
|
+
if slash_idx != -1:
|
|
86
|
+
return literal_prefix[: slash_idx + 1]
|
|
87
|
+
return ""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def get_rule_prefixes(rule: InvariantRule) -> list[str]:
|
|
91
|
+
"""Extract path prefixes associated with a rule."""
|
|
92
|
+
if not rule.scope.paths:
|
|
93
|
+
return [""]
|
|
94
|
+
prefixes: list[str] = []
|
|
95
|
+
for pat in rule.scope.paths:
|
|
96
|
+
pfx = extract_path_prefix(pat)
|
|
97
|
+
if pfx == "":
|
|
98
|
+
return [""]
|
|
99
|
+
prefixes.append(pfx)
|
|
100
|
+
return prefixes
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def match_rule(
|
|
104
|
+
rule: InvariantRule,
|
|
105
|
+
target_path: str,
|
|
106
|
+
target_language: Optional[str] = None,
|
|
107
|
+
) -> bool:
|
|
108
|
+
"""Check if an invariant rule applies to the specified file and language."""
|
|
109
|
+
norm_path = normalize_path(target_path)
|
|
110
|
+
|
|
111
|
+
# Path scope check
|
|
112
|
+
if rule.scope.paths:
|
|
113
|
+
path_matched = any(match_path(pat, norm_path) for pat in rule.scope.paths)
|
|
114
|
+
if not path_matched:
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
# Language scope check
|
|
118
|
+
if rule.scope.languages:
|
|
119
|
+
resolved_lang = target_language.lower() if target_language else infer_language(norm_path)
|
|
120
|
+
if not resolved_lang:
|
|
121
|
+
return False
|
|
122
|
+
if not any(lang.lower() == resolved_lang for lang in rule.scope.languages):
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
return True
|
aos/mcp.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Model Context Protocol (MCP) server for universal agent harness integration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from aos.autopsy import inscribe_candidate, synthesize_candidate_rule
|
|
10
|
+
from aos.blackboard import post_event, read_events
|
|
11
|
+
from aos.engine import RuleEngine
|
|
12
|
+
from aos.enforcer import enforce_all
|
|
13
|
+
|
|
14
|
+
PROTOCOL_VERSION = "2024-11-05"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_tool_definitions() -> list[dict[str, Any]]:
|
|
18
|
+
return [
|
|
19
|
+
{
|
|
20
|
+
"name": "aos_rules_check",
|
|
21
|
+
"description": "Evaluate target file paths against active substrate invariant rules.",
|
|
22
|
+
"inputSchema": {
|
|
23
|
+
"type": "object",
|
|
24
|
+
"properties": {
|
|
25
|
+
"files": {
|
|
26
|
+
"type": "array",
|
|
27
|
+
"items": {"type": "string"},
|
|
28
|
+
"description": "List of file paths to check.",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
"required": ["files"],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "aos_rules_list",
|
|
36
|
+
"description": "List all discovered invariant rules in the substrate.",
|
|
37
|
+
"inputSchema": {
|
|
38
|
+
"type": "object",
|
|
39
|
+
"properties": {
|
|
40
|
+
"status": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"enum": ["active", "candidate", "archive"],
|
|
43
|
+
"description": "Filter by rule status.",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "aos_autopsy",
|
|
50
|
+
"description": "Inscribe a new candidate invariant rule derived from a failure autopsy.",
|
|
51
|
+
"inputSchema": {
|
|
52
|
+
"type": "object",
|
|
53
|
+
"properties": {
|
|
54
|
+
"id": {"type": "string", "description": "Unique rule identifier."},
|
|
55
|
+
"statement": {"type": "string", "description": "Invariant directive statement."},
|
|
56
|
+
"rationale": {"type": "string", "description": "Technical justification."},
|
|
57
|
+
"paths": {"type": "array", "items": {"type": "string"}, "description": "Path globs."},
|
|
58
|
+
"languages": {"type": "array", "items": {"type": "string"}, "description": "Languages."},
|
|
59
|
+
"incident_id": {"type": "string", "description": "Incident reference."},
|
|
60
|
+
},
|
|
61
|
+
"required": ["id", "statement", "rationale", "paths"],
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"name": "aos_blackboard_post",
|
|
66
|
+
"description": "Broadcast an event or intent announcement on the local peer blackboard.",
|
|
67
|
+
"inputSchema": {
|
|
68
|
+
"type": "object",
|
|
69
|
+
"properties": {
|
|
70
|
+
"type": {"type": "string", "description": "Event type."},
|
|
71
|
+
"sender": {"type": "string", "description": "Agent identifier."},
|
|
72
|
+
"payload": {"type": "object", "description": "Arbitrary payload mapping."},
|
|
73
|
+
},
|
|
74
|
+
"required": ["type", "sender", "payload"],
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def handle_tool_call(name: str, args: dict[str, Any], root_dir: Path) -> str:
|
|
81
|
+
if name == "aos_rules_check":
|
|
82
|
+
files = args.get("files", [])
|
|
83
|
+
engine = RuleEngine(root_dir=root_dir)
|
|
84
|
+
matches = engine.match_files(files, status="active")
|
|
85
|
+
violations = enforce_all(files, root_dir=root_dir)
|
|
86
|
+
|
|
87
|
+
output: dict[str, Any] = {"matches": {}, "violations": []}
|
|
88
|
+
for f, rules in matches.items():
|
|
89
|
+
output["matches"][f] = [r.to_dict() for r in rules]
|
|
90
|
+
output["violations"] = [
|
|
91
|
+
{
|
|
92
|
+
"file_path": v.file_path,
|
|
93
|
+
"rule_id": v.rule_id,
|
|
94
|
+
"message": v.message,
|
|
95
|
+
"enforcement": v.enforcement,
|
|
96
|
+
"line": v.line_number,
|
|
97
|
+
}
|
|
98
|
+
for v in violations
|
|
99
|
+
]
|
|
100
|
+
return json.dumps(output, indent=2)
|
|
101
|
+
|
|
102
|
+
elif name == "aos_rules_list":
|
|
103
|
+
status = args.get("status")
|
|
104
|
+
engine = RuleEngine(root_dir=root_dir)
|
|
105
|
+
rules = engine.get_rules(status=status)
|
|
106
|
+
return json.dumps([r.to_dict() for r in rules], indent=2)
|
|
107
|
+
|
|
108
|
+
elif name == "aos_autopsy":
|
|
109
|
+
rule = synthesize_candidate_rule(
|
|
110
|
+
rule_id=args["id"],
|
|
111
|
+
statement=args["statement"],
|
|
112
|
+
rationale=args["rationale"],
|
|
113
|
+
paths=args["paths"],
|
|
114
|
+
languages=args.get("languages", []),
|
|
115
|
+
incident_id=args.get("incident_id", "inc-mcp"),
|
|
116
|
+
inscribing_agent="mcp-client-agent",
|
|
117
|
+
)
|
|
118
|
+
saved = inscribe_candidate(rule, substrate_dir=root_dir / ".agents" / "substrate")
|
|
119
|
+
return json.dumps({"status": "inscribed", "path": str(saved)})
|
|
120
|
+
|
|
121
|
+
elif name == "aos_blackboard_post":
|
|
122
|
+
ev = post_event(
|
|
123
|
+
{
|
|
124
|
+
"type": args["type"],
|
|
125
|
+
"sender": args["sender"],
|
|
126
|
+
"payload": args["payload"],
|
|
127
|
+
},
|
|
128
|
+
root_dir=root_dir,
|
|
129
|
+
)
|
|
130
|
+
return json.dumps({"status": "posted", "event_id": ev.event_id})
|
|
131
|
+
|
|
132
|
+
return json.dumps({"error": f"Unknown tool: {name}"})
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def run_mcp_server(root_dir: Path | str = ".") -> None:
|
|
136
|
+
"""Run the JSON-RPC stdio loop."""
|
|
137
|
+
root = Path(root_dir)
|
|
138
|
+
for line in sys.stdin:
|
|
139
|
+
line_str = line.strip()
|
|
140
|
+
if not line_str:
|
|
141
|
+
continue
|
|
142
|
+
try:
|
|
143
|
+
req = json.loads(line_str)
|
|
144
|
+
except Exception:
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
msg_id = req.get("id")
|
|
148
|
+
method = req.get("method")
|
|
149
|
+
params = req.get("params", {})
|
|
150
|
+
|
|
151
|
+
if method == "initialize":
|
|
152
|
+
res = {
|
|
153
|
+
"jsonrpc": "2.0",
|
|
154
|
+
"id": msg_id,
|
|
155
|
+
"result": {
|
|
156
|
+
"protocolVersion": PROTOCOL_VERSION,
|
|
157
|
+
"capabilities": {"tools": {}},
|
|
158
|
+
"serverInfo": {"name": "aos-mcp-server", "version": "0.1.0"},
|
|
159
|
+
},
|
|
160
|
+
}
|
|
161
|
+
elif method == "tools/list":
|
|
162
|
+
res = {
|
|
163
|
+
"jsonrpc": "2.0",
|
|
164
|
+
"id": msg_id,
|
|
165
|
+
"result": {"tools": get_tool_definitions()},
|
|
166
|
+
}
|
|
167
|
+
elif method == "tools/call":
|
|
168
|
+
tool_name = params.get("name", "")
|
|
169
|
+
tool_args = params.get("arguments", {})
|
|
170
|
+
text_result = handle_tool_call(tool_name, tool_args, root)
|
|
171
|
+
res = {
|
|
172
|
+
"jsonrpc": "2.0",
|
|
173
|
+
"id": msg_id,
|
|
174
|
+
"result": {
|
|
175
|
+
"content": [{"type": "text", "text": text_result}],
|
|
176
|
+
},
|
|
177
|
+
}
|
|
178
|
+
elif method == "notifications/initialized":
|
|
179
|
+
continue
|
|
180
|
+
else:
|
|
181
|
+
res = {
|
|
182
|
+
"jsonrpc": "2.0",
|
|
183
|
+
"id": msg_id,
|
|
184
|
+
"error": {"code": -32601, "message": f"Method not found: {method}"},
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
sys.stdout.write(json.dumps(res) + "\n")
|
|
188
|
+
sys.stdout.flush()
|