Audit¶
Audit logging proves the agent (not the human) performed each action. Events land in ~/.entraclaw/audit/<YYYY-MM-DD>.jsonl.
The body prompt's security section makes audit non-overridable: if audit can't record, the action doesn't proceed. Security-sensitive operations (cross-tenant Teams sends, chat member adds, memory mutations) must call audit_log before execution. Fail-closed is enforced by the call sites, not by the audit module itself.
log_event¶
src/entraclaw/tools/audit.py:
def log_event(
action: str,
resource: str,
outcome: str = "success",
agent_id: str | None = None,
metadata: dict | None = None,
attribution_type: str = "agent",
) -> dict
Write an audit event and return it. Writes a single JSON line to ~/.entraclaw/audit/<YYYY-MM-DD>.jsonl. Also logs via the standard entraclaw.tools.audit logger.
| Field | Source | Notes |
|---|---|---|
event_id |
uuid.uuid4() |
Globally unique. |
timestamp |
datetime.now(UTC).isoformat() |
UTC always. |
agent_id |
Argument or credential store | Falls back to "unknown" on any retrieval failure. |
action |
Argument | e.g. "file_read", "teams_send", "identity_promote". |
resource |
Argument | What is being acted on. |
outcome |
Argument | "success", "pending", "failure". |
attribution_type |
Argument | "agent", "delegated-human", "none". |
metadata |
Argument | Arbitrary JSON-safe dict. |
attribution_type¶
Distinguishes agent actions from delegated-human actions:
agent— action performed as the Agent User identity (default).delegated-human— action performed using the human's delegated token (delegatedmode).none— unauthenticated / unknown identity.
audit_log (MCP tool)¶
The audit_log MCP tool is a thin wrapper around log_event, exposed so the LLM can record its own deliberation steps:
@mcp.tool()
def audit_log(
action: str,
resource: str,
outcome: str = "success",
metadata: str = "{}",
) -> str
Call BEFORE performing any action on the user's behalf. The metadata argument is a JSON string (not a dict) because MCP-tool params are scalar-only. The wrapper parses it.
_audit_graph_call middleware¶
src/entraclaw/tools/files.py wraps every Graph Files call in an async context manager:
@asynccontextmanager
async def _audit_graph_call(
verb: str,
resource: str,
*,
metadata: dict | None = None,
) -> AsyncIterator[None]
Emits outcome="pending" before the body runs and "success" or "failure" after. Replaces nine ad-hoc log_event blocks. Use the same pattern in new tool modules.
On exception, the metadata is enriched with error (class name) and message (str) before the failure event is written, then the exception re-raises unchanged.
Fail-closed semantics¶
From prompts/anatomy/security.md:
Audit before acting. Security-sensitive operations (adding a chat member, cross-tenant sends, changing memory state) must be logged via
audit_logbefore execution. If audit writes fail, the action does not proceed.
log_event itself does not enforce this — it returns the event on success and only raises on actual write failure (disk full, permission denied). Call sites that need fail-closed must catch and abort. The Files tool wrapper above is the canonical pattern.
Reading the audit log¶
# Today's events
cat ~/.entraclaw/audit/$(date -u +%Y-%m-%d).jsonl | jq .
# All events for a specific resource
cat ~/.entraclaw/audit/*.jsonl | jq 'select(.resource == "chat_19:abc...@unq.gbl.spaces")'
# Failures only
cat ~/.entraclaw/audit/*.jsonl | jq 'select(.outcome == "failure")'
run_daily_summary reads the audit log and the interaction log to build the 5pm PDT triage email — see src/entraclaw/tools/daily_summary.py.