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/curator.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Evolutionary rule curator for pruning, subsumption, and substrate hygiene."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
from aos.blackboard import post_event
|
|
11
|
+
from aos.discovery import discover_rules
|
|
12
|
+
from aos.models import InvariantRule, Scope
|
|
13
|
+
from aos.validator import validate_rule_dict
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class CurationAction:
|
|
18
|
+
rule_id: str
|
|
19
|
+
action_type: str
|
|
20
|
+
reason: str
|
|
21
|
+
superseded_by: Optional[str] = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def is_path_subsumed(sub_pattern: str, super_pattern: str) -> bool:
|
|
25
|
+
"""Check if super_pattern universally covers sub_pattern."""
|
|
26
|
+
if super_pattern in ("**/*", "**", "*"):
|
|
27
|
+
return True
|
|
28
|
+
if sub_pattern == super_pattern:
|
|
29
|
+
return True
|
|
30
|
+
if super_pattern.endswith("/**") and sub_pattern.startswith(super_pattern[:-3]):
|
|
31
|
+
return True
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def is_scope_subsumed(sub_scope: Scope, super_scope: Scope) -> bool:
|
|
36
|
+
"""Determine whether sub_scope is a strict subset of super_scope."""
|
|
37
|
+
# Language coverage check
|
|
38
|
+
if super_scope.languages:
|
|
39
|
+
if not sub_scope.languages:
|
|
40
|
+
return False
|
|
41
|
+
super_langs = {l.lower() for l in super_scope.languages}
|
|
42
|
+
sub_langs = {l.lower() for l in sub_scope.languages}
|
|
43
|
+
if not sub_langs.issubset(super_langs):
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
# Path coverage check
|
|
47
|
+
if super_scope.paths:
|
|
48
|
+
if not sub_scope.paths:
|
|
49
|
+
return False
|
|
50
|
+
for sub_p in sub_scope.paths:
|
|
51
|
+
covered = any(is_path_subsumed(sub_p, sup_p) for sup_p in super_scope.paths)
|
|
52
|
+
if not covered:
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def archive_rule(
|
|
59
|
+
rule: InvariantRule,
|
|
60
|
+
reason: str,
|
|
61
|
+
substrate_dir: Path | str = ".agents/substrate",
|
|
62
|
+
event_log_path: Optional[Path | str] = None,
|
|
63
|
+
) -> Path:
|
|
64
|
+
"""Move a rule to archive status and log a blackboard event."""
|
|
65
|
+
sub_path = Path(substrate_dir)
|
|
66
|
+
if (sub_path / ".agents" / "substrate").is_dir():
|
|
67
|
+
sub_path = sub_path / ".agents" / "substrate"
|
|
68
|
+
|
|
69
|
+
archive_dir = sub_path / "archive"
|
|
70
|
+
archive_dir.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
|
|
72
|
+
rule_dict = rule.to_dict()
|
|
73
|
+
rule_dict["status"] = "archive"
|
|
74
|
+
if "provenance" not in rule_dict:
|
|
75
|
+
rule_dict["provenance"] = {}
|
|
76
|
+
rule_dict["provenance"]["last_verified_at"] = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
77
|
+
|
|
78
|
+
target_file = archive_dir / f"{rule.id}.yaml"
|
|
79
|
+
with open(target_file, "w", encoding="utf-8") as f:
|
|
80
|
+
yaml.safe_dump(rule_dict, f, sort_keys=False)
|
|
81
|
+
|
|
82
|
+
if rule.file_path and rule.file_path.is_file():
|
|
83
|
+
rule.file_path.unlink()
|
|
84
|
+
|
|
85
|
+
rule.file_path = target_file
|
|
86
|
+
rule.status = "archive"
|
|
87
|
+
|
|
88
|
+
post_event(
|
|
89
|
+
{
|
|
90
|
+
"type": "RULE_ARCHIVED",
|
|
91
|
+
"sender": "evolutionary-curator-v1",
|
|
92
|
+
"payload": {
|
|
93
|
+
"rule_id": rule.id,
|
|
94
|
+
"reason": reason,
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
event_log_path=event_log_path,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
return target_file
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def curate_substrate(
|
|
104
|
+
substrate_dir: Path | str = ".agents/substrate",
|
|
105
|
+
dry_run: bool = False,
|
|
106
|
+
event_log_path: Optional[Path | str] = None,
|
|
107
|
+
) -> list[CurationAction]:
|
|
108
|
+
"""Inspect active rules for duplicate or subsumed invariants and prune them."""
|
|
109
|
+
rules, _ = discover_rules(substrate_dir, statuses={"active"})
|
|
110
|
+
actions: list[CurationAction] = []
|
|
111
|
+
|
|
112
|
+
# Sort rules deterministically by id
|
|
113
|
+
rules = sorted(rules, key=lambda r: r.id)
|
|
114
|
+
|
|
115
|
+
# Detect subsumption: Rule B has identical statement but narrower scope than Rule A
|
|
116
|
+
archived_ids: set[str] = set()
|
|
117
|
+
for i, r_super in enumerate(rules):
|
|
118
|
+
if r_super.id in archived_ids:
|
|
119
|
+
continue
|
|
120
|
+
for j, r_sub in enumerate(rules):
|
|
121
|
+
if i == j or r_sub.id in archived_ids:
|
|
122
|
+
continue
|
|
123
|
+
# Compare statements
|
|
124
|
+
same_statement = r_super.invariant.statement.strip().lower() == r_sub.invariant.statement.strip().lower()
|
|
125
|
+
if same_statement and is_scope_subsumed(r_sub.scope, r_super.scope):
|
|
126
|
+
reason = f"Subsumed by broader rule '{r_super.id}' with identical invariant statement."
|
|
127
|
+
action = CurationAction(
|
|
128
|
+
rule_id=r_sub.id,
|
|
129
|
+
action_type="ARCHIVE_SUBSUMED",
|
|
130
|
+
reason=reason,
|
|
131
|
+
superseded_by=r_super.id,
|
|
132
|
+
)
|
|
133
|
+
actions.append(action)
|
|
134
|
+
archived_ids.add(r_sub.id)
|
|
135
|
+
if not dry_run:
|
|
136
|
+
archive_rule(
|
|
137
|
+
r_sub,
|
|
138
|
+
reason=reason,
|
|
139
|
+
substrate_dir=substrate_dir,
|
|
140
|
+
event_log_path=event_log_path,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return actions
|
aos/daemon.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Autonomous background daemon for reactive auditing, event interception, and curation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import time
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
from aos.blackboard import BlackboardEvent, get_default_event_log, post_event, read_events
|
|
10
|
+
from aos.curator import curate_substrate
|
|
11
|
+
from aos.engine import RuleEngine
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class DaemonState:
|
|
16
|
+
processed_event_ids: set[str] = field(default_factory=set)
|
|
17
|
+
injected_intent_ids: set[str] = field(default_factory=set)
|
|
18
|
+
curation_interval_seconds: float = 300.0
|
|
19
|
+
last_curation_timestamp: float = 0.0
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SubstrateDaemon:
|
|
23
|
+
"""Continuous background engine that audits announced intents and curates rules."""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
root_dir: Path | str = ".",
|
|
28
|
+
event_log_path: Optional[Path | str] = None,
|
|
29
|
+
poll_interval: float = 2.0,
|
|
30
|
+
curation_interval: float = 60.0,
|
|
31
|
+
) -> None:
|
|
32
|
+
self.root_dir = Path(root_dir)
|
|
33
|
+
self.event_log_path = Path(event_log_path) if event_log_path else get_default_event_log(self.root_dir)
|
|
34
|
+
self.poll_interval = poll_interval
|
|
35
|
+
self.state = DaemonState(curation_interval_seconds=curation_interval)
|
|
36
|
+
self.engine = RuleEngine(root_dir=self.root_dir)
|
|
37
|
+
|
|
38
|
+
def process_event(self, event: BlackboardEvent) -> Optional[BlackboardEvent]:
|
|
39
|
+
"""Inspect a single event and generate autonomous reactions if required."""
|
|
40
|
+
if event.event_id in self.state.processed_event_ids:
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
self.state.processed_event_ids.add(event.event_id)
|
|
44
|
+
|
|
45
|
+
# Reactive Auditor: intercept INTENT_ANNOUNCEMENT
|
|
46
|
+
if event.type == "INTENT_ANNOUNCEMENT":
|
|
47
|
+
payload = event.payload
|
|
48
|
+
intent_id = payload.get("intent_id", "")
|
|
49
|
+
target_files = payload.get("target_files", [])
|
|
50
|
+
|
|
51
|
+
if intent_id and intent_id not in self.state.injected_intent_ids and target_files:
|
|
52
|
+
self.engine.reload()
|
|
53
|
+
matched_rules_map = self.engine.match_files(target_files, status="active")
|
|
54
|
+
|
|
55
|
+
# Collect unique rules
|
|
56
|
+
applicable_rule_ids: list[str] = []
|
|
57
|
+
required_checks: list[str] = []
|
|
58
|
+
seen_rules: set[str] = set()
|
|
59
|
+
|
|
60
|
+
for f_path, rules in matched_rules_map.items():
|
|
61
|
+
for r in rules:
|
|
62
|
+
if r.id not in seen_rules:
|
|
63
|
+
seen_rules.add(r.id)
|
|
64
|
+
applicable_rule_ids.append(r.id)
|
|
65
|
+
check_desc = f"rule:{r.id} ({r.invariant.enforcement})"
|
|
66
|
+
if r.invariant.max_blast_radius_lines is not None:
|
|
67
|
+
check_desc += f", max_diff_lines:{r.invariant.max_blast_radius_lines}"
|
|
68
|
+
required_checks.append(check_desc)
|
|
69
|
+
|
|
70
|
+
injection_event = post_event(
|
|
71
|
+
{
|
|
72
|
+
"type": "INVARIANT_INJECTION",
|
|
73
|
+
"sender": "daemon-auditor-agent",
|
|
74
|
+
"payload": {
|
|
75
|
+
"intent_id": intent_id,
|
|
76
|
+
"applicable_rules": applicable_rule_ids,
|
|
77
|
+
"required_checks": required_checks,
|
|
78
|
+
"target_files": target_files,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
event_log_path=self.event_log_path,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
self.state.injected_intent_ids.add(intent_id)
|
|
85
|
+
self.state.processed_event_ids.add(injection_event.event_id)
|
|
86
|
+
return injection_event
|
|
87
|
+
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
def tick(self) -> list[BlackboardEvent]:
|
|
91
|
+
"""Execute a single polling step: process new events and check curation schedule."""
|
|
92
|
+
generated: list[BlackboardEvent] = []
|
|
93
|
+
|
|
94
|
+
# 1. Read existing events and process unprocessed ones
|
|
95
|
+
events = read_events(event_log_path=self.event_log_path)
|
|
96
|
+
for ev in events:
|
|
97
|
+
reaction = self.process_event(ev)
|
|
98
|
+
if reaction is not None:
|
|
99
|
+
generated.append(reaction)
|
|
100
|
+
|
|
101
|
+
# 2. Check periodic curation tick
|
|
102
|
+
now = time.time()
|
|
103
|
+
if (now - self.state.last_curation_timestamp) >= self.state.curation_interval_seconds:
|
|
104
|
+
self.state.last_curation_timestamp = now
|
|
105
|
+
curate_substrate(
|
|
106
|
+
substrate_dir=self.root_dir / ".agents" / "substrate",
|
|
107
|
+
event_log_path=self.event_log_path,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return generated
|
|
111
|
+
|
|
112
|
+
def run(self, max_ticks: Optional[int] = None) -> None:
|
|
113
|
+
"""Run the daemon loop until interrupted or max_ticks reached."""
|
|
114
|
+
ticks_done = 0
|
|
115
|
+
try:
|
|
116
|
+
while max_ticks is None or ticks_done < max_ticks:
|
|
117
|
+
self.tick()
|
|
118
|
+
ticks_done += 1
|
|
119
|
+
time.sleep(self.poll_interval)
|
|
120
|
+
except KeyboardInterrupt:
|
|
121
|
+
pass
|
aos/discovery.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Rule discovery and loading from the substrate filesystem."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
from aos.models import InvariantRule
|
|
9
|
+
from aos.validator import validate_rule_dict
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def load_rule_file(file_path: Path) -> tuple[Optional[InvariantRule], list[str]]:
|
|
13
|
+
"""Load, parse, and validate a single rule file from disk."""
|
|
14
|
+
if not file_path.is_file():
|
|
15
|
+
return None, [f"File not found: {file_path}"]
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
content = file_path.read_text(encoding="utf-8")
|
|
19
|
+
data = yaml.safe_load(content)
|
|
20
|
+
except Exception as exc:
|
|
21
|
+
return None, [f"Failed to parse YAML in {file_path}: {exc}"]
|
|
22
|
+
|
|
23
|
+
if not isinstance(data, dict):
|
|
24
|
+
return None, [f"Expected mapping in {file_path}, got {type(data).__name__}"]
|
|
25
|
+
|
|
26
|
+
is_valid, errors = validate_rule_dict(data)
|
|
27
|
+
if not is_valid:
|
|
28
|
+
prefixed = [f"Validation failure in {file_path}: {err}" for err in errors]
|
|
29
|
+
return None, prefixed
|
|
30
|
+
|
|
31
|
+
rule = InvariantRule.from_dict(data, file_path=file_path)
|
|
32
|
+
return rule, []
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def discover_rules(
|
|
36
|
+
substrate_dir: Path | str,
|
|
37
|
+
statuses: Optional[set[str]] = None,
|
|
38
|
+
) -> tuple[list[InvariantRule], list[str]]:
|
|
39
|
+
"""Discover all invariant rules in the substrate directory tree.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
substrate_dir: Path to .agents/substrate/ or repository root.
|
|
43
|
+
statuses: Optional filter for rule status (e.g. {'active'}).
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
tuple: (list of valid InvariantRule instances, list of error messages)
|
|
47
|
+
"""
|
|
48
|
+
base = Path(substrate_dir)
|
|
49
|
+
if (base / ".agents" / "substrate").is_dir():
|
|
50
|
+
base = base / ".agents" / "substrate"
|
|
51
|
+
|
|
52
|
+
rules: list[InvariantRule] = []
|
|
53
|
+
diagnostics: list[str] = []
|
|
54
|
+
|
|
55
|
+
subdirs = ["active", "candidate", "archive"]
|
|
56
|
+
target_dirs = [base / sub for sub in subdirs if (base / sub).is_dir()]
|
|
57
|
+
|
|
58
|
+
# If none of the subdirs exist, inspect base itself
|
|
59
|
+
if not target_dirs and base.is_dir():
|
|
60
|
+
target_dirs = [base]
|
|
61
|
+
|
|
62
|
+
for directory in target_dirs:
|
|
63
|
+
for ext in ("*.yaml", "*.yml"):
|
|
64
|
+
for file_path in sorted(directory.glob(ext)):
|
|
65
|
+
rule, errors = load_rule_file(file_path)
|
|
66
|
+
if errors:
|
|
67
|
+
diagnostics.extend(errors)
|
|
68
|
+
if rule is not None:
|
|
69
|
+
if statuses is None or rule.status in statuses:
|
|
70
|
+
rules.append(rule)
|
|
71
|
+
|
|
72
|
+
return rules, diagnostics
|