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/blackboard.py ADDED
@@ -0,0 +1,114 @@
1
+ """Asynchronous local blackboard event bus for peer-to-peer agent coordination."""
2
+
3
+ from __future__ import annotations
4
+ import json
5
+ import time
6
+ import uuid
7
+ from dataclasses import dataclass, field
8
+ from datetime import datetime, timezone
9
+ from pathlib import Path
10
+ from typing import Any, Iterator, Optional
11
+
12
+
13
+ @dataclass
14
+ class BlackboardEvent:
15
+ type: str
16
+ sender: str
17
+ payload: dict[str, Any] = field(default_factory=dict)
18
+ event_id: str = ""
19
+ timestamp: str = ""
20
+
21
+ def __post_init__(self) -> None:
22
+ if not self.event_id:
23
+ prefix = int(time.time() * 1000)
24
+ short_id = uuid.uuid4().hex[:6]
25
+ self.event_id = f"evt-{prefix}-{short_id}"
26
+ if not self.timestamp:
27
+ self.timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
28
+
29
+ def to_dict(self) -> dict[str, Any]:
30
+ return {
31
+ "event_id": self.event_id,
32
+ "timestamp": self.timestamp,
33
+ "type": self.type,
34
+ "sender": self.sender,
35
+ "payload": self.payload,
36
+ }
37
+
38
+ @classmethod
39
+ def from_dict(cls, data: dict[str, Any]) -> BlackboardEvent:
40
+ return cls(
41
+ event_id=str(data.get("event_id", "")),
42
+ timestamp=str(data.get("timestamp", "")),
43
+ type=str(data.get("type", "")),
44
+ sender=str(data.get("sender", "")),
45
+ payload=dict(data.get("payload", {})),
46
+ )
47
+
48
+
49
+ def get_default_event_log(root_dir: Path | str = ".") -> Path:
50
+ """Resolve path to default events.jsonl ledger."""
51
+ root = Path(root_dir)
52
+ return root / ".agents" / "blackboard" / "events.jsonl"
53
+
54
+
55
+ def post_event(
56
+ event: BlackboardEvent | dict[str, Any],
57
+ event_log_path: Optional[Path | str] = None,
58
+ root_dir: Path | str = ".",
59
+ ) -> BlackboardEvent:
60
+ """Append an event to the local blackboard ledger."""
61
+ if isinstance(event, dict):
62
+ event_obj = BlackboardEvent.from_dict(event)
63
+ # Ensure event_id and timestamp are populated
64
+ if not event_obj.event_id or not event_obj.timestamp:
65
+ event_obj = BlackboardEvent(
66
+ type=event.get("type", "UNKNOWN"),
67
+ sender=event.get("sender", "anonymous"),
68
+ payload=event.get("payload", {}),
69
+ event_id=event.get("event_id", ""),
70
+ timestamp=event.get("timestamp", ""),
71
+ )
72
+ else:
73
+ event_obj = event
74
+
75
+ target_path = Path(event_log_path) if event_log_path else get_default_event_log(root_dir)
76
+ target_path.parent.mkdir(parents=True, exist_ok=True)
77
+
78
+ line = json.dumps(event_obj.to_dict(), separators=(",", ":"))
79
+ with open(target_path, "a", encoding="utf-8") as f:
80
+ f.write(line + "\n")
81
+
82
+ return event_obj
83
+
84
+
85
+ def read_events(
86
+ event_log_path: Optional[Path | str] = None,
87
+ event_type: Optional[str] = None,
88
+ sender: Optional[str] = None,
89
+ root_dir: Path | str = ".",
90
+ ) -> list[BlackboardEvent]:
91
+ """Read all events from the ledger matching optional filters."""
92
+ target_path = Path(event_log_path) if event_log_path else get_default_event_log(root_dir)
93
+ if not target_path.is_file():
94
+ return []
95
+
96
+ events: list[BlackboardEvent] = []
97
+ with open(target_path, "r", encoding="utf-8") as f:
98
+ for line in f:
99
+ stripped = line.strip()
100
+ if not stripped:
101
+ continue
102
+ try:
103
+ data = json.loads(stripped)
104
+ ev = BlackboardEvent.from_dict(data)
105
+ if event_type is not None and ev.type != event_type:
106
+ continue
107
+ if sender is not None and ev.sender != sender:
108
+ continue
109
+ events.append(ev)
110
+ except Exception:
111
+ # Ignore corrupted lines to prevent ledger read halts
112
+ continue
113
+
114
+ return events
aos/ci.py ADDED
@@ -0,0 +1,201 @@
1
+ """CI/CD integration engine for GitHub Actions and GitLab pipelines."""
2
+
3
+ from __future__ import annotations
4
+ import os
5
+ import subprocess
6
+ import sys
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Optional
10
+
11
+ from aos.enforcer import Violation, enforce_all
12
+ from aos.harness import sync_harnesses
13
+
14
+
15
+ @dataclass
16
+ class CIReport:
17
+ checked_files: list[str]
18
+ violations: list[Violation]
19
+ status: str
20
+
21
+ def to_markdown(self) -> str:
22
+ lines = [
23
+ "## AOS Invariant Verification Report",
24
+ "",
25
+ f"**Status**: {'PASSED' if self.status == 'passed' else 'FAILED'}",
26
+ f"**Files Evaluated**: {len(self.checked_files)}",
27
+ f"**Violations Detected**: {len(self.violations)}",
28
+ "",
29
+ ]
30
+ if not self.violations:
31
+ lines.append("All modified files strictly comply with active substrate invariants.")
32
+ else:
33
+ lines.append("### Invariant Violations")
34
+ for v in self.violations:
35
+ loc = f":{v.line_number}" if v.line_number else ""
36
+ lines.append(f"* **[{v.rule_id}]** `{v.file_path}{loc}`: {v.message} (`{v.enforcement}`)")
37
+ lines.append("")
38
+ return "\n".join(lines)
39
+
40
+
41
+ def get_git_changed_files(base_ref: str = "origin/main", root_dir: Path | str = ".") -> list[str]:
42
+ """Retrieve list of files changed relative to base ref."""
43
+ root = Path(root_dir)
44
+ try:
45
+ res = subprocess.run(
46
+ ["git", "diff", "--name-only", "--diff-filter=ACM", f"{base_ref}...HEAD"],
47
+ cwd=str(root),
48
+ capture_output=True,
49
+ text=True,
50
+ check=True,
51
+ )
52
+ return [f.strip() for f in res.stdout.splitlines() if f.strip()]
53
+ except Exception:
54
+ # Fallback to local staged or modified files
55
+ try:
56
+ res = subprocess.run(
57
+ ["git", "diff", "--name-only", "--diff-filter=ACM", "HEAD"],
58
+ cwd=str(root),
59
+ capture_output=True,
60
+ text=True,
61
+ check=True,
62
+ )
63
+ return [f.strip() for f in res.stdout.splitlines() if f.strip()]
64
+ except Exception:
65
+ return []
66
+
67
+
68
+ def run_ci_check(
69
+ files: Optional[list[str]] = None,
70
+ base_ref: str = "origin/main",
71
+ auto_sync: bool = False,
72
+ root_dir: Path | str = ".",
73
+ ) -> CIReport:
74
+ """Run comprehensive CI check, sync harnesses if requested, and write step summary."""
75
+ root = Path(root_dir)
76
+ target_files = files if files else get_git_changed_files(base_ref=base_ref, root_dir=root)
77
+
78
+ if auto_sync:
79
+ sync_harnesses(root_dir=root)
80
+
81
+ violations = enforce_all(target_files, root_dir=root)
82
+ reject_violations = [v for v in violations if v.enforcement == "reject_diff"]
83
+
84
+ status = "failed" if reject_violations else "passed"
85
+ report = CIReport(
86
+ checked_files=target_files,
87
+ violations=violations,
88
+ status=status,
89
+ )
90
+
91
+ # Write to GITHUB_STEP_SUMMARY if available
92
+ summary_path = os.getenv("GITHUB_STEP_SUMMARY")
93
+ if summary_path:
94
+ try:
95
+ with open(summary_path, "a", encoding="utf-8") as f:
96
+ f.write(report.to_markdown() + "\n")
97
+ except Exception:
98
+ pass
99
+
100
+ return report
101
+
102
+
103
+ def review_pr_diff(diff_content: str, root_dir: Path | str = ".") -> dict[str, Any]:
104
+ """Analyze unified git diff, evaluate against active invariants, and format PR review comments."""
105
+ from aos.engine import RuleEngine
106
+ from aos.enforcer import check_content_violations
107
+ from aos.harness import get_rule_guidance
108
+
109
+ root = Path(root_dir)
110
+ engine = RuleEngine(root_dir=root)
111
+ comments: list[dict[str, Any]] = []
112
+ current_file: Optional[str] = None
113
+ file_lines: list[str] = []
114
+
115
+ def evaluate_file(file_path: str, lines: list[str]) -> None:
116
+ content = "\n".join(lines)
117
+ matched = engine.match_file(file_path, status="active")
118
+ violations = check_content_violations(file_path, content, matched)
119
+ for v in violations:
120
+ rule_obj = next((r for r in matched if r.id == v.rule_id), None)
121
+ compliant = get_rule_guidance(rule_obj)[0] if rule_obj else None
122
+ sugg = f"\n\n**Compliant Pattern**: `{compliant}`" if compliant else ""
123
+ comments.append({
124
+ "file_path": v.file_path,
125
+ "line_number": v.line_number or 1,
126
+ "rule_id": v.rule_id,
127
+ "message": v.message,
128
+ "comment_body": f"### Invariant Barrier [{v.rule_id}]\n\n{v.message}{sugg}",
129
+ })
130
+
131
+ for line in diff_content.splitlines():
132
+ if line.startswith("+++ b/"):
133
+ if current_file and file_lines:
134
+ evaluate_file(current_file, file_lines)
135
+ current_file = line[6:].strip()
136
+ file_lines = []
137
+ elif current_file and line.startswith("+") and not line.startswith("+++"):
138
+ file_lines.append(line[1:])
139
+
140
+ if current_file and file_lines:
141
+ evaluate_file(current_file, file_lines)
142
+
143
+ return {
144
+ "status": "changes_requested" if comments else "approved",
145
+ "comments_count": len(comments),
146
+ "comments": comments,
147
+ }
148
+
149
+
150
+ DEFAULT_GITHUB_WORKFLOW = """name: AOS Guardrails
151
+
152
+ on:
153
+ pull_request:
154
+ branches: [ master, main ]
155
+ push:
156
+ branches: [ master, main ]
157
+
158
+ jobs:
159
+ verify-invariants:
160
+ name: Verify Substrate Invariants
161
+ runs-on: ubuntu-latest
162
+ steps:
163
+ - name: Checkout repository
164
+ uses: actions/checkout@v4
165
+ with:
166
+ fetch-depth: 0
167
+
168
+ - name: Set up Python
169
+ uses: actions/setup-python@v5
170
+ with:
171
+ python-version: "3.12"
172
+
173
+ - name: Install AOS
174
+ run: |
175
+ python -m pip install --upgrade pip
176
+ pip install -e .
177
+
178
+ - name: Verify Invariants
179
+ run: |
180
+ python -m aos ci run --auto-sync
181
+ """
182
+
183
+
184
+ def install_ci_workflow(root_dir: Path | str = ".") -> Path:
185
+ """Install GitHub Actions workflow in .github/workflows/aos-guardrails.yml."""
186
+ root = Path(root_dir)
187
+ wf_dir = root / ".github" / "workflows"
188
+ wf_dir.mkdir(parents=True, exist_ok=True)
189
+ target = wf_dir / "aos-guardrails.yml"
190
+ target.write_text(DEFAULT_GITHUB_WORKFLOW, encoding="utf-8")
191
+ return target
192
+
193
+
194
+ def uninstall_ci_workflow(root_dir: Path | str = ".") -> bool:
195
+ """Remove GitHub Actions workflow if installed."""
196
+ target = Path(root_dir) / ".github" / "workflows" / "aos-guardrails.yml"
197
+ if target.is_file():
198
+ target.unlink()
199
+ return True
200
+ return False
201
+