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
21 changes: 17 additions & 4 deletions datamind/capabilities/hooks/audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@

Secret redaction: arg values whose KEY matches a redaction regex
(`api_key`, `password`, `token`, `secret`, ...) are replaced with
`"[REDACTED]"` before logging. Values are not scanned (we trust the
caller to put secrets in well-named fields, which is true across
DataMind's own tools).
`"[REDACTED]"` before logging. Credential-shaped values in exception
text are redacted as well because provider and transport errors commonly
include the rejected request detail.

Concurrency: writes are serialised via an asyncio.Lock per AuditLogHook
instance. For multi-process deployments use one process per profile
Expand All @@ -45,6 +45,11 @@
r"(?i)(api[_-]?key|password|passwd|token|secret|authorization|bearer|access[_-]?key|client[_-]?secret)"
)
_REDACTED = "[REDACTED]"
_BEARER_TEXT_RE = re.compile(r"(?i)\bBearer\s+([^\s,;]+)")
_SECRET_TEXT_RE = re.compile(
r"(?i)\b(api[_-]?key|password|passwd|token|secret|authorization|access[_-]?key|client[_-]?secret)\b"
r"(\s*[:=]\s*)([^\s,;]+)"
)

_HASH_HEX_LEN = 16 # truncated SHA-256 hex; 64 bits of collision resistance

Expand Down Expand Up @@ -74,6 +79,14 @@ def _redact(value: Any) -> Any:
return value


def _redact_text(value: str) -> str:
"""Redact common credential formats embedded in diagnostic text."""
value = _BEARER_TEXT_RE.sub(f"Bearer {_REDACTED}", value)
return _SECRET_TEXT_RE.sub(
lambda match: f"{match.group(1)}{match.group(2)}{_REDACTED}", value
)


def _decision_to_record(decision: HookDecision) -> dict[str, Any]:
if isinstance(decision, Allow):
return {"kind": "allow"}
Expand Down Expand Up @@ -163,7 +176,7 @@ async def post_tool_use(
"error": (
None
if error is None
else f"{type(error).__name__}: {error}"
else _redact_text(f"{type(error).__name__}: {error}")
),
}
await self._append(record)
Expand Down
20 changes: 20 additions & 0 deletions datamind/tests/test_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,26 @@ async def test_audit_log_redacts_secret_keys(tmp_path: Path):
assert rec["args"]["username"] == "ann"


@pytest.mark.asyncio
async def test_audit_log_redacts_secrets_in_errors(tmp_path: Path):
audit = tmp_path / "audit.jsonl"
hook = AuditLogHook(audit_path=audit)
await hook.post_tool_use(
_ctx(),
"fake_tool",
{},
result=None,
error=RuntimeError(
"request failed: api_key=sk-live-secret Authorization: Bearer bearer-secret"
),
)

rec = json.loads(audit.read_text().splitlines()[-1])
assert "sk-live-secret" not in rec["error"]
assert "bearer-secret" not in rec["error"]
assert "[REDACTED]" in rec["error"]


@pytest.mark.asyncio
async def test_audit_log_records_denied_decision(tmp_path: Path):
audit = tmp_path / "audit.jsonl"
Expand Down