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/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
|
+
|