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.
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()