Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- **Integrations layout ([#19](https://github.com/ARPAHLS/aura/issues/19))** — top-level `integrations/README.md` stack index; Skillware `mock_tools.py` / `live_tools.py` entrypoints; LangGraph stub; doc links from getting-started and provider READMEs.

### Added

- **Audit pipeline example ([#23](https://github.com/ARPAHLS/aura/issues/23))** — `examples/audit_pipeline.py`: two sessions, audit report receipt, programmatic compare, hash-chain verify, CLI follow-ups (mock ToolHost; no Skillware required).
- **Audit pipeline example ([#23](https://github.com/ARPAHLS/aura/issues/23))** — `examples/audit_pipeline.py`: two sessions, audit report receipt, programmatic compare, hash-chain verify, OTel export assert, CLI follow-ups (mock ToolHost; no Skillware required).

- **Tailored metrics snapshot example** — `examples/10-observer-metrics-snapshot/`: AURA-native Monitor preset + `metrics_snapshot` observer note; documents export hook for playbooks without third-party KPI skills.

- **Profile `spectrum` block (preview)** — optional agent profile field; ingress summary; `Spectrum.coat()` / `planes()` helpers ([#27](https://github.com/ARPAHLS/aura/issues/27) docs preview).
- **Host stress simulation** — `scripts/aura_host_stress_sim.py` + `tests/test_host_stress_sim.py`: sixteen scenarios (loose/tight/tailored coats, spectrum low/mid/high/full bind, Skillware paths, sequencer, observers, export).

- **Host stress simulation** — `scripts/aura_host_stress_sim.py` + `tests/test_host_stress_sim.py`: multi-scenario AURA+Skillware host runs (coats, observers, chains, sequencer, export).
- **Coat flow report** — `scripts/aura_coat_flow_report.py`: structured session breakdown per spectrum level (agent, skills, timeline, audit receipt).

- **Audit pipeline CI assert** — `tests/test_audit_pipeline_otel.py` verifies OTel export bytes from `examples/audit_pipeline.py`.

### Changed

- **Spectrum enforcement ([#27](https://github.com/ARPAHLS/aura/issues/27))** — `spectrum.level` on profiles injects egress rules at session open (`low` audit-only, `high` skill allowlist, `full` sequencer `step_id` required); `sequencer_required` constraint; `aura agent set --spectrum-level`; `aura agent show` includes `effective_spectrum`; `aura config show` includes `spectrum_levels` reference; `session.open` carries spectrum summary.

- **Skillware compatibility** — optional extra `skillware>=0.5.4,<0.6` (auto patch within 0.5.x; conscious bump at 0.6); docs for `SkillContext`, named chains vs AURA sequencer, version policy in [skillware-integration.md](docs/skillware-integration.md).

- **`docs/comparison.md` ([#39](https://github.com/ARPAHLS/aura/issues/39))** — refresh for shipped egress, audit report, and hash chain; loose / tight / tailored coat section; host-agnostic ToolHost framing (Skillware as reference adapter); trim stale v0.1 / intercept-roadmap voice; fix Gatekeeper roadmap link ([#56](https://github.com/ARPAHLS/aura/issues/56)); INDEX and getting-started link blurbs.
Expand Down
5 changes: 3 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ Follow the [Code of Conduct](CODE_OF_CONDUCT.md). We welcome autonomous logical
```

- Shared fixtures: **`tests/conftest.py`** (`aura_home`, `run_aura` for CLI subprocess tests).
- Full suite is **64+ tests** across `test_core.py`, `test_core_gaps.py`, `test_v02.py`, `test_v03.py`, `test_cli.py`, `test_examples_smoke.py` — see [TESTING.md](docs/TESTING.md).
- Full suite is **130+ tests** across core, CLI, examples smoke, spectrum enforcement, host stress sim, and coat flow report — see [TESTING.md](docs/TESTING.md).
- CI runs on PRs via [`.github/workflows/ci.yml`](.github/workflows/ci.yml) (Python **3.10–3.13** matrix; gate job **`lint-test`**: pytest with coverage report, black, flake8). Steps are defined once in [`.github/workflows/reusable-test.yml`](.github/workflows/reusable-test.yml). Each matrix cell also runs `pip-audit` as a warn-only dependency check; its findings or audit errors do not fail the gate or block a PR. See [TESTING.md](docs/TESTING.md) for the exact commands.
- Wait for green checks before requesting review.

Expand All @@ -152,7 +152,8 @@ Pure internal refactors with no user-visible effect may omit CHANGELOG; ask on t
| :--- | :--- |
| Public SDK (`aura/api.py`, `SessionRun` methods) | `docs/getting-started.md`, `docs/using-aura.md`, `docs/concepts.md`, tests, CHANGELOG |
| Session / spine event shape | `spec/aura-event.schema.json` (if applicable), `docs/outputs.md`, tests, CHANGELOG |
| Agent profile / registry fields | `docs/trust-paths.md`, `aura/agents/profile.py` persistence, tests, CHANGELOG |
| Agent profile / registry fields | `docs/trust-paths.md`, `docs/aura-levels.md`, `docs/using-aura.md`, `aura/agents/profile.py` persistence, tests, CHANGELOG |
| `spectrum.level` / enforcement rules | `aura/core/spectrum_enforcement.py`, `docs/aura-levels.md`, `docs/comparison.md`, `docs/ROADMAP.md`, `tests/test_spectrum_enforcement.py`, stress sim / flow report scripts, CHANGELOG |
| Constraint rule types | `docs/concepts.md`, `aura/core/constraints.py` tests, CHANGELOG |
| Sequencer step model | `spec/sequencer.schema.json`, `docs/sequencer.md`, `tests/test_v02.py`, CHANGELOG |
| Skillware host / egress | `integrations/skillware/` (when shipped), `docs/skillware-integration.md` redirect, CHANGELOG |
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ with ag.session() as run:
print(run.exports)
```

CLI: `aura agent create`, `aura run`, `aura export`, `aura report show`, `aura identity show`, `aura compare`, `aura export-otel`, `aura verify chain`.
CLI: `aura agent create`, `aura agent set --spectrum-level`, `aura run`, `aura export`, `aura report show`, `aura identity show`, `aura compare`, `aura export-otel`, `aura verify chain`.

→ [getting-started.md](docs/getting-started.md) · [onboarding.md](docs/onboarding.md) · [examples/](examples/)

Expand Down
3 changes: 3 additions & 0 deletions aura/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ def _build_session(
except ValueError:
session_mode = SessionMode.SCRIPT
merged_rules = list(agent.profile.rules)
from aura.core.spectrum_enforcement import enforcement_rules

merged_rules.extend(enforcement_rules(agent.profile))
if rules:
merged_rules.extend(rules)
from aura.sequencer.spec import merge_sequencer_spec
Expand Down
69 changes: 66 additions & 3 deletions aura/cli/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,17 @@ def cmd_agent_show(name: str, *, console: Console | None = None) -> int:
else:
console.print(message, style="bold #FF9AA2")
return 1
payload = json.dumps(profile.to_dict(), indent=2)
from aura.core.spectrum_enforcement import effective_spectrum, enforcement_rules

payload = profile.to_dict()
spec = effective_spectrum(profile)
payload["effective_spectrum"] = spec.summary()
payload["effective_spectrum"]["enforcement_rules"] = enforcement_rules(profile)
text = json.dumps(payload, indent=2)
if console is None:
print(payload)
print(text)
else:
console.print(payload, style="dim")
console.print(text, style="dim")
return 0


Expand Down Expand Up @@ -358,6 +364,31 @@ def _parse_key_value_pairs(
return result, None


def _parse_spectrum_json(raw: str) -> dict:
data = json.loads(raw)
if not isinstance(data, dict):
raise ValueError("--spectrum-json must be a JSON object")
return data


def _spectrum_from_cli(
*,
spectrum_level: str | None,
spectrum_services: list[str] | None,
spectrum_json: str | None,
) -> dict | None:
if spectrum_json is not None:
return _parse_spectrum_json(spectrum_json)
if spectrum_level is None and not spectrum_services:
return None
block: dict = {}
if spectrum_level is not None:
block["level"] = spectrum_level.strip().lower()
if spectrum_services:
block["services"] = list(spectrum_services)
return block


def cmd_agent_set(
key: str,
*,
Expand All @@ -370,6 +401,9 @@ def cmd_agent_set(
ids: list[str] | None = None,
rules_file: Path | None = None,
rules_json: str | None = None,
spectrum_level: str | None = None,
spectrum_services: list[str] | None = None,
spectrum_json: str | None = None,
console: Console | None = None,
) -> int:
reg = AgentRegistry()
Expand Down Expand Up @@ -443,6 +477,22 @@ def cmd_agent_set(
if rules is not None:
updates["rules"] = rules

try:
spectrum_block = _spectrum_from_cli(
spectrum_level=spectrum_level,
spectrum_services=spectrum_services,
spectrum_json=spectrum_json,
)
except ValueError as exc:
message = str(exc)
if console is None:
print(message, file=sys.stderr)
else:
console.print(message, style="bold #FF9AA2")
return 2
if spectrum_block is not None:
updates["spectrum"] = spectrum_block

if not updates:
message = "no fields to update (pass --ref, --purpose, --skill, etc.)"
if console is None:
Expand Down Expand Up @@ -485,6 +535,19 @@ def cmd_config_show(*, console: Console | None = None) -> int:
{"layer": label, "path": str(path), "loaded": loaded}
for label, path, loaded in config_sources(cfg)
],
"spectrum_note": (
"Spectrum bind is per agent profile — use `aura agent show <id>` "
"for effective_spectrum and enforcement_rules."
),
"spectrum_levels": {
"low": {
"coat": "loose",
"enforcement": "audit only — explicit profile rules still apply",
},
"mid": {"coat": "tight", "enforcement": "explicit profile rules only (default)"},
"high": {"coat": "tight", "enforcement": "auto allow_tools from declared skills"},
"full": {"coat": "tailored", "enforcement": "high bind + tool.call requires step_id"},
},
}
text = json.dumps(payload, indent=2)
if console is None:
Expand Down
2 changes: 1 addition & 1 deletion aura/cli/help_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
("aura agent create <name>", "register agent profile (optional --ref)"),
("aura agent list", "list aura_id, agent_ref, and name"),
("aura agent show <id>", "JSON profile by name, agent_ref, or aura_id"),
("aura agent set <id> …", "update ref, purpose, skills, variables, rules"),
("aura agent set <id> …", "update ref, purpose, skills, variables, rules, spectrum"),
],
_DOCS_ONBOARDING,
),
Expand Down
15 changes: 15 additions & 0 deletions aura/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,18 @@ def build_parser() -> argparse.ArgumentParser:
)
set_p.add_argument("--rules-file", type=Path, help="JSON file with rules array")
set_p.add_argument("--rules-json", help="Inline JSON array of rules")
set_p.add_argument(
"--spectrum-level",
choices=["low", "mid", "high", "full"],
help="Spectrum bind level (audit-only to full)",
)
set_p.add_argument(
"--spectrum-service",
action="append",
dest="spectrum_services",
help="Spectrum service id (repeatable; replaces services list)",
)
set_p.add_argument("--spectrum-json", help="Inline JSON object for spectrum block")

config_p = sub.add_parser("config", help="Configuration")
config_sub = config_p.add_subparsers(dest="config_command")
Expand Down Expand Up @@ -209,6 +221,9 @@ def _dispatch_agent(args: argparse.Namespace) -> int:
ids=args.ids,
rules_file=args.rules_file,
rules_json=args.rules_json,
spectrum_level=args.spectrum_level,
spectrum_services=args.spectrum_services,
spectrum_json=args.spectrum_json,
)
print("usage: aura agent {create|list|show|set}", file=sys.stderr)
return 1
Expand Down
44 changes: 35 additions & 9 deletions aura/core/constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,16 @@ def _tool_name(payload: dict[str, Any]) -> str | None:
return payload.get("tool") or payload.get("name") or payload.get("tool_name")


def _tool_in_allowlist(payload: dict[str, Any], allowed: list[str]) -> bool:
tool = _tool_name(payload)
skill_id = payload.get("skill_id")
if tool and tool in allowed:
return True
if skill_id and str(skill_id) in allowed:
return True
return False


def _token_count(payload: dict[str, Any]) -> int:
for key in ("tokens", "token_count", "total_tokens"):
if key in payload:
Expand Down Expand Up @@ -140,15 +150,15 @@ def _rule_allow_tools(ctx: ConstraintContext, rule: dict[str, Any]) -> Constrain
allowed = rule.get("tools") or rule.get("allow") or []
if not allowed:
return None
tool = _tool_name(ctx.payload)
if tool not in allowed:
return ConstraintResult(
passed=False,
rule=rule,
message=f"Tool not allowed: {tool}",
blocked=True,
)
return ConstraintResult(passed=True, rule=rule, message="tool allowed")
if _tool_in_allowlist(ctx.payload, allowed):
return ConstraintResult(passed=True, rule=rule, message="tool allowed")
tool = _tool_name(ctx.payload) or ctx.payload.get("skill_id")
return ConstraintResult(
passed=False,
rule=rule,
message=f"Tool not allowed: {tool}",
blocked=True,
)


def _rule_deny_tools(ctx: ConstraintContext, rule: dict[str, Any]) -> ConstraintResult | None:
Expand All @@ -166,9 +176,25 @@ def _rule_deny_tools(ctx: ConstraintContext, rule: dict[str, Any]) -> Constraint
return None


def _rule_sequencer_required(
ctx: ConstraintContext, rule: dict[str, Any]
) -> ConstraintResult | None:
if ctx.event_kind not in ("tool.call", "action.request"):
return None
if ctx.payload.get("step_id"):
return ConstraintResult(passed=True, rule=rule, message="sequencer step present")
return ConstraintResult(
passed=False,
rule=rule,
message="Tool call outside declared sequencer step (spectrum full bind)",
blocked=True,
)


_BUILTIN: dict[str, Any] = {
"max_tokens_per_step": _rule_max_tokens,
"confirm_before": _rule_confirm_before,
"allow_tools": _rule_allow_tools,
"deny_tools": _rule_deny_tools,
"sequencer_required": _rule_sequencer_required,
}
6 changes: 6 additions & 0 deletions aura/core/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ def open(
)
self._open = True
self._attach_profile_observers()
from aura.core.spectrum_enforcement import effective_spectrum, enforcement_rules

spectrum = effective_spectrum(self.profile)
spectrum_meta = spectrum.summary()
spectrum_meta["enforcement_rule_count"] = len(enforcement_rules(self.profile))
self.emit(
"membrane.ingress",
ingress_event_payload(self.profile, self.mode.value, self.snapshot_hash),
Expand All @@ -127,6 +132,7 @@ def open(
"purpose": self.profile.purpose,
"policy_version": self.profile.policy_version,
"agent_ref": self.profile.agent_ref,
"spectrum": spectrum_meta,
},
)
if self._operator_identity:
Expand Down
2 changes: 1 addition & 1 deletion aura/core/spectrum.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def coat(self) -> str:
return COAT_BY_LEVEL.get(self.level.lower(), "tight")

def planes(self) -> dict[str, bool]:
"""Three planes: audit (always), enforce, escalate — docs + #27 preview."""
"""Three planes: audit (always), enforce, escalate."""
level = self.level.lower()
return {
"audit": True,
Expand Down
90 changes: 90 additions & 0 deletions aura/core/spectrum_enforcement.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""Spectrum-derived enforcement rules merged at session open."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

from aura.core.spectrum import Spectrum

if TYPE_CHECKING:
from aura.agents.profile import AgentProfile


def effective_spectrum(profile: AgentProfile) -> Spectrum:
"""Resolve spectrum for a profile (defaults to mid when unset)."""
if profile.spectrum:
return Spectrum.from_profile({"spectrum": profile.spectrum})
return Spectrum.from_manifest({})


def allowed_tool_ids(profile: AgentProfile) -> list[str]:
"""Tool and skill ids declared on the profile for high/full bind."""
tools: list[str] = list(profile.skills or [])
sequencer = profile.sequencer or {}
for step in sequencer.get("steps") or []:
if not isinstance(step, dict):
continue
ref = step.get("ref")
if ref:
tools.append(str(ref))
config = step.get("config") or {}
if isinstance(config, dict):
tool = config.get("tool")
if tool:
tools.append(str(tool))
seen: set[str] = set()
ordered: list[str] = []
for item in tools:
if item not in seen:
seen.add(item)
ordered.append(item)
return ordered


def enforcement_rules(profile: AgentProfile) -> list[dict[str, Any]]:
"""
Rules injected from spectrum.level (explicit profile rules always apply too).

low — audit only (no auto bind)
mid — explicit profile rules only
high — allow_tools from declared skills / sequencer refs
full — high + tool.call must carry step_id (sequencer bind)
"""
spectrum = effective_spectrum(profile)
level = spectrum.level.lower()
rules: list[dict[str, Any]] = []

if level in {"high", "full"}:
allowed = allowed_tool_ids(profile)
if allowed:
rules.append(
{
"type": "allow_tools",
"tools": allowed,
"source": "spectrum",
"level": level,
}
)

if level == "full":
rules.append(
{
"type": "sequencer_required",
"source": "spectrum",
"level": level,
}
)

return rules


def merge_rules_with_spectrum(
profile: AgentProfile,
session_rules: list[dict[str, Any]] | None = None,
) -> list[dict[str, Any]]:
"""Profile rules + spectrum enforcement + optional session overrides."""
merged = list(profile.rules or [])
merged.extend(enforcement_rules(profile))
if session_rules:
merged.extend(session_rules)
return merged
Loading
Loading