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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- **Verified operator identity ([#55](https://github.com/ARPAHLS/aura/issues/55))** — optional identity adapters (manual, mock, OIDC, Auth0); `identity.bound` spine event; `ids.operator` on all event trailers; export redaction; `aura identity show`; profile `types` with `role: identity`.

- **Session lifecycle invariants ([#15](https://github.com/ARPAHLS/aura/issues/15))** — strict closed-session errors (`SessionClosedError`, `SessionAlreadyOpenError`); `export=False` builds in-memory `summary` and `audit_report` on `SessionRun`; atomic summary + OTel commit on export; frozen `declared_rules` / `open_snapshot_hash` at open (runtime rule merges via skill bind still apply to constraints); `trace_id` on summary export.

- **Documentation sweep ([#14](https://github.com/ARPAHLS/aura/issues/14))** — `docs/INDEX.md` three-tier entry (Start / Build / Decide + Optional vision); demoted narrative, three-rings, aura-levels, field-services; refreshed architecture, concepts, stack-position, field-services shipped vs planned; fixed stale v0.2 voice in getting-started and concepts.
Expand Down
4 changes: 2 additions & 2 deletions 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 compare`, `aura export-otel`, `aura verify chain`.
CLI: `aura agent create`, `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 All @@ -125,7 +125,7 @@ CLI: `aura agent create`, `aura run`, `aura export`, `aura report show`, `aura c
| **Index** | [docs/INDEX.md](docs/INDEX.md) — full doc map (Start / Build / Decide) |
| **Start** | [onboarding.md](docs/onboarding.md) · [getting-started.md](docs/getting-started.md) · [concepts.md](docs/concepts.md) · [using-aura.md](docs/using-aura.md) |
| **Integration** | [reference-tool-host-capstone.md](docs/guides/reference-tool-host-capstone.md) · [guides/aura-on-skillware.md](docs/guides/aura-on-skillware.md) · [skillware-integration.md](docs/skillware-integration.md) · [sequencer.md](docs/sequencer.md) |
| **Identity & audit** | [trust-paths.md](docs/trust-paths.md) · [outputs.md](docs/outputs.md) |
| **Identity & audit** | [trust-paths.md](docs/trust-paths.md) · [integrations/identity/](integrations/identity/) · [outputs.md](docs/outputs.md) |
| **Compare & position** | [comparison.md](docs/comparison.md) · [ROADMAP.md](docs/ROADMAP.md) |
| **Contribute** | [CONTRIBUTING.md](CONTRIBUTING.md) · [Agent workflow](docs/contributing/ai_native_workflow.md) · [TESTING.md](docs/TESTING.md) · [PUBLISHING.md](docs/PUBLISHING.md) · [CHANGELOG.md](CHANGELOG.md) |

Expand Down
8 changes: 8 additions & 0 deletions aura/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
AgentHandle,
ApprovalRequired,
ExportError,
IdentityOptions,
IdentityRequiredError,
IdentityVerificationError,
OperatorIdentityAdapter,
SessionClosedError,
SessionNotOpenError,
SessionRun,
Expand All @@ -29,4 +33,8 @@
"SessionClosedError",
"SessionNotOpenError",
"ExportError",
"IdentityOptions",
"OperatorIdentityAdapter",
"IdentityRequiredError",
"IdentityVerificationError",
]
3 changes: 3 additions & 0 deletions aura/agents/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class AgentProfile:
skills: list[str] = field(default_factory=list)
sequencer: dict[str, Any] | None = None
observers: list[dict[str, Any]] = field(default_factory=list)
types: list[dict[str, Any]] = field(default_factory=list)
default_mode: str = "script"
archived: bool = False

Expand All @@ -37,6 +38,7 @@ def to_dict(self) -> dict[str, Any]:
"skills": self.skills,
"sequencer": self.sequencer,
"observers": self.observers,
"types": self.types,
"default_mode": self.default_mode,
"archived": self.archived,
}
Expand All @@ -55,6 +57,7 @@ def from_dict(cls, data: dict[str, Any]) -> AgentProfile:
skills=list(data.get("skills") or []),
sequencer=data.get("sequencer"),
observers=list(data.get("observers") or []),
types=list(data.get("types") or []),
default_mode=data.get("default_mode", "script"),
archived=bool(data.get("archived", False)),
)
Expand Down
2 changes: 2 additions & 0 deletions aura/agents/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ def create(
skills: list[str] | None = None,
sequencer: dict[str, Any] | None = None,
observers: list[dict[str, Any]] | None = None,
types: list[dict[str, Any]] | None = None,
ids: dict[str, Any] | None = None,
default_mode: str = "script",
) -> AgentProfile:
Expand Down Expand Up @@ -117,6 +118,7 @@ def create(
skills=skills or [],
sequencer=sequencer,
observers=observers or [],
types=types or [],
default_mode=default_mode,
)
self.save(profile)
Expand Down
17 changes: 16 additions & 1 deletion aura/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
from aura.core.errors import ExportError, SessionClosedError, SessionNotOpenError
from aura.core.session import Session, SessionMode
from aura.exporters.jsonl import build_session_summary, export_session
from aura.identity.bind import IdentityOptions
from aura.identity.errors import IdentityRequiredError, IdentityVerificationError
from aura.identity.protocol import OperatorIdentityAdapter


@dataclass
Expand Down Expand Up @@ -102,11 +105,19 @@ def session(
rules: list[dict[str, Any]] | None = None,
export: bool | None = None,
sequencer: dict[str, Any] | None = None,
identity_adapter: OperatorIdentityAdapter | None = None,
operator: dict[str, Any] | None = None,
identity: dict[str, Any] | None = None,
) -> Iterator[SessionRun]:
cfg = get_config()
session = _build_session(self, mode, rules, sequencer)
run = SessionRun(_session=session)
session.open(cfg.sessions_dir())
identity_options = IdentityOptions(
adapter=identity_adapter,
operator=operator,
config=dict(identity or {}),
)
session.open(cfg.sessions_dir(), identity_options=identity_options)
token = _current_run.set(run)
try:
yield run
Expand Down Expand Up @@ -189,4 +200,8 @@ def list_agents(include_archived: bool = False) -> list[AgentProfile]:
"SessionNotOpenError",
"ExportError",
"current_session",
"IdentityOptions",
"OperatorIdentityAdapter",
"IdentityRequiredError",
"IdentityVerificationError",
]
25 changes: 25 additions & 0 deletions aura/cli/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,31 @@ def cmd_export_otel(session_id: str, *, console: Console | None = None) -> int:
return 0


def cmd_identity_show(*, console: Console | None = None) -> int:
from aura.config import get_config

cfg = get_config()
payload = {
"identity": cfg.values.get("identity") or {},
"identity_required": cfg.values.get("identity_required", False),
"identity_export_pii": cfg.values.get("identity_export_pii", False),
"identity_redact_fields": cfg.values.get("identity_redact_fields", []),
"env_hints": {
"AURA_OIDC_TOKEN": bool(__import__("os").environ.get("AURA_OIDC_TOKEN")),
"AURA_AUTH0_DOMAIN": bool(__import__("os").environ.get("AURA_AUTH0_DOMAIN")),
"AURA_MOCK_OPERATOR_SUBJECT": bool(
__import__("os").environ.get("AURA_MOCK_OPERATOR_SUBJECT")
),
},
}
text = json.dumps(payload, indent=2)
if console is None:
print(text)
else:
console.print(text, style="dim")
return 0


def cmd_compare(session_a: str, session_b: str, *, console: Console | None = None) -> int:
from aura.config import get_config

Expand Down
3 changes: 3 additions & 0 deletions aura/cli/help_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
_DOCS_CLI = "https://github.com/ARPAHLS/aura/blob/main/docs/using-aura.md"
_DOCS_GETTING_STARTED = "https://github.com/ARPAHLS/aura/blob/main/docs/getting-started.md"
_DOCS_TESTING = "https://github.com/ARPAHLS/aura/blob/main/docs/TESTING.md"
_DOCS_IDENTITY = "https://github.com/ARPAHLS/aura/blob/main/integrations/identity/README.md"

HELP_GROUPS: List[Tuple[str, List[Tuple[str, str]], str]] = [
(
Expand Down Expand Up @@ -41,6 +42,7 @@
("aura paths set-project <dir>", "persist default project directory"),
("aura paths set-storage global|project", "persist storage mode"),
("aura config show", "merged global + project YAML and paths"),
("aura identity show", "operator identity adapter config (optional)"),
("aura --home <dir>", "override AURA_HOME for this invocation"),
("aura --project <dir>", "one-shot project-scoped .aura/ storage"),
],
Expand Down Expand Up @@ -72,6 +74,7 @@
"aura agent set demo-bot --ref acme/demo --purpose compliance",
"aura agent list",
"aura config show",
"aura identity show",
"aura paths set-project .",
"aura run cli-runner path/to/script.py",
"aura export aura_sess_01H...",
Expand Down
9 changes: 9 additions & 0 deletions aura/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ def build_parser() -> argparse.ArgumentParser:
compare_p.add_argument("session_a", help="First session id")
compare_p.add_argument("session_b", help="Second session id")

identity_p = sub.add_parser("identity", help="Operator identity adapters and config")
identity_sub = identity_p.add_subparsers(dest="identity_command")
identity_sub.add_parser("show", help="Show merged identity configuration")

verify_p = sub.add_parser("verify", help="Verify exported session data")
verify_sub = verify_p.add_subparsers(dest="verify_command")
chain_p = verify_sub.add_parser("chain", help="Validate a JSONL audit hash chain")
Expand Down Expand Up @@ -160,6 +164,11 @@ def dispatch(args: argparse.Namespace) -> int:
return commands.cmd_export_otel(args.session_id)
if args.command == "compare":
return commands.cmd_compare(args.session_a, args.session_b)
if args.command == "identity":
if args.identity_command == "show" or args.identity_command is None:
return commands.cmd_identity_show()
print("usage: aura identity show", file=sys.stderr)
return 1
if args.command == "verify":
if args.verify_command == "chain":
return commands.cmd_verify_chain(args.path)
Expand Down
3 changes: 3 additions & 0 deletions aura/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
"storage": "global", # global | project
"default_session_mode": "script",
"export_on_close": True,
"identity_required": False,
"identity_export_pii": False,
"identity_redact_fields": ["email", "name", "phone"],
}


Expand Down
53 changes: 44 additions & 9 deletions aura/core/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
ConstraintViolation,
)
from aura.core.spine import AuditSpine
from aura.identity.bind import IdentityOptions, bind_operator_identity
from aura.identity.models import OperatorIdentity
from aura.membrane.ingress import ingress_event_payload
from aura.observers.base import Observer, get_registry

Expand Down Expand Up @@ -61,6 +63,8 @@ class Session:
_goal_reached: bool = False
_declared_rules: list[dict[str, Any]] = field(default_factory=list)
_open_snapshot_hash: str | None = None
_operator_identity: OperatorIdentity | None = None
_identity_ids_overlay: dict[str, Any] = field(default_factory=dict)

def __setattr__(self, name: str, value: Any) -> None:
if (
Expand All @@ -87,14 +91,22 @@ def _ensure_active(self) -> None:
if not self._open or not self.spine:
raise SessionNotOpenError(self.session_id)

def open(self, sessions_dir: Path) -> None:
def open(
self,
sessions_dir: Path,
*,
identity_options: IdentityOptions | None = None,
) -> None:
if self._closed:
raise SessionClosedError(self.session_id)
if self._open:
raise SessionAlreadyOpenError(self.session_id)
self.snapshot_hash = _snapshot_hash(self.profile, self.rules)
self._open_snapshot_hash = self.snapshot_hash
self._declared_rules = copy.deepcopy(self.rules)

bind_operator_identity(self, identity_options)

self._log_path = sessions_dir / f"{self.session_id}.jsonl"
self.spine = AuditSpine(
session_id=self.session_id,
Expand All @@ -117,6 +129,26 @@ def open(self, sessions_dir: Path) -> None:
"agent_ref": self.profile.agent_ref,
},
)
if self._operator_identity:
self.emit("identity.bound", self._operator_identity.bind_payload())

def agent_ids_trailer(self) -> dict[str, Any]:
"""Profile trailer merged with session-scoped operator identity."""
base = self.profile.id_trailer()
if not self._identity_ids_overlay:
return base
ids = dict(base.get("ids") or {})
for key, value in self._identity_ids_overlay.items():
if isinstance(ids.get(key), dict) and isinstance(value, dict):
ids[key] = {**ids[key], **value}
else:
ids[key] = value
base["ids"] = ids
return base

@property
def operator_identity(self) -> OperatorIdentity | None:
return self._operator_identity

def _attach_profile_observers(self) -> None:
for entry in self.profile.observers:
Expand Down Expand Up @@ -194,29 +226,29 @@ def emit(
"rule": exc.rule,
"pending_event": {"kind": kind, "payload": payload or {}},
},
agent_ids=self.profile.id_trailer(),
agent_ids=self.agent_ids_trailer(),
)
raise
except ConstraintViolation as exc:
self.spine.append(
"constraint.violated",
{"message": str(exc), "rule": exc.rule, "event": exc.event},
agent_ids=self.profile.id_trailer(),
agent_ids=self.agent_ids_trailer(),
)
raise

event = self.spine.append(
kind,
payload or {},
agent_ids=self.profile.id_trailer(),
agent_ids=self.agent_ids_trailer(),
task_id=self.task_id,
step_id=step_id,
)
if constraint_results:
self.spine.append(
"constraint.passed",
{"results": constraint_results, "for_event": event.event_id},
agent_ids=self.profile.id_trailer(),
agent_ids=self.agent_ids_trailer(),
)
self._dispatch_observers(event.to_dict())
return event.to_dict()
Expand All @@ -241,7 +273,7 @@ def require_approval(
"message": message,
"rule": rule,
},
agent_ids=self.profile.id_trailer(),
agent_ids=self.agent_ids_trailer(),
step_id=step_id,
)
raise ApprovalRequired(request_id, message, rule)
Expand All @@ -251,12 +283,15 @@ def approve(self, request_id: str, *, principal: str | None = None) -> None:
self._approved.add(request_id)
if self.spine:
payload: dict[str, Any] = {"request_id": request_id}
if principal:
payload["principal"] = principal
effective_principal = principal
if effective_principal is None and self._operator_identity:
effective_principal = self._operator_identity.subject
if effective_principal:
payload["principal"] = effective_principal
self.spine.append(
"constraint.approved",
payload,
agent_ids=self.profile.id_trailer(),
agent_ids=self.agent_ids_trailer(),
)

def _dispatch_observers(self, event: dict[str, Any]) -> None:
Expand Down
13 changes: 9 additions & 4 deletions aura/exporters/jsonl.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
from aura.core.audit_report import AuditReport, AuditReportBuilder
from aura.core.conformance import ConformanceEngine, ConformanceReport
from aura.core.errors import ExportError
from aura.identity.redaction import redact_summary
from aura.core.session import Session
from aura.exporters.otel import export_otel_jsonl
from aura.exporters.otel import export_otel_jsonl, redact_events_for_export
from aura.core.spine import AuditSpine


Expand Down Expand Up @@ -39,7 +40,7 @@ def build_session_summary(
policy_version=session.profile.policy_version,
)

return {
summary = {
"session_id": session.session_id,
"aura_id": session.profile.aura_id,
"agent_ref": session.profile.agent_ref,
Expand All @@ -49,13 +50,17 @@ def build_session_summary(
"snapshot_hash": session.snapshot_hash,
"open_snapshot_hash": session.open_snapshot_hash,
"trace_id": session.trace_id,
"agent_ids": session.profile.id_trailer(),
"agent_ids": session.agent_ids_trailer(),
"identity": (
session.operator_identity.to_operator_dict() if session.operator_identity else None
),
"purpose": session.profile.purpose,
"conformance": conformance.to_dict() if conformance else None,
"audit_report": audit_report.to_dict() if audit_report else None,
"event_count": len(session.spine.stream()) if session.spine else 0,
"log": str(session.log_path) if session.log_path else None,
}
return redact_summary(summary)


def _atomic_replace(staging_path: Path, final_path: Path) -> None:
Expand All @@ -70,7 +75,7 @@ def _write_staging_json(path: Path, payload: dict[str, Any]) -> None:
def _write_staging_otel(session_id: str, sessions_dir: Path, staging_path: Path) -> None:
log_path = sessions_dir / f"{session_id}.jsonl"
events = AuditSpine.read_jsonl(log_path)
export_otel_jsonl(events, staging_path)
export_otel_jsonl(redact_events_for_export(events), staging_path)


def export_session(
Expand Down
Loading
Loading