Skip to main content

3tears-agent-audit

Unified audit envelope, fire-and-forget publish helper, and the erasure rule and hub request for anonymizing audit records, for the 3tears platform.

Purpose

Single AuditEvent envelope + single publish_audit helper used by every domain (workspace, rbac, memory, custom tools) so the audit pipeline is one subject tree, one consumer, one table, one admin query API. Replaces domain-specific envelopes (WorkspaceAuditEnvelope, RbacAuditEnvelope) that produced slightly-different wire shapes per domain and made cross-domain audit queries require a UNION.

Publishing is the package's core: AuditEvent and publish_audit import nothing beyond the NATS client. On a hub deployment the HUB persists every event into its platform table, and the package's persistence code is never used; an agent there erases a person from that table by asking the hub (request_audit_anonymization), and the hub applies the erasure rule.

A deployment with NO hub (one application owning its own control plane) owns its audit table, and threetears.agent.audit.persist is its persister:

  • ensure_audit_events_table(db) creates the table, with every envelope field and ip_address, and migrates an existing one: every column the insert names beyond id and the four required fields (timestamp, event_type, action, correlation_id) is added if missing, with the default the CREATE gives it. Idempotency is on the envelope id.
  • start_audit_persister(nats, db, durable=..., storage="memory") runs a shared durable pull consumer with a dead-letter subject. A malformed event is dropped; a database fault is retried, then dead-lettered.
    • storage must match every other declarer of the audit stream.
    • durable must be unique per table.
  • prune_audit_events(db, older_than=...) is batched retention.
  • anonymize_audit_rows(db, actor_user_ids=...) is erasure: every row and id is kept, and details and ip_address are rewritten by the platform's rule and no other -- it takes no replacement anonymizer. A family whose keys are safe in this deployment declares them with declare_safe_detail_keys. It returns the same AuditAnonymization the hub path does, and never answers the hub's anonymize subject.

db is anything with asyncpg's execute/fetch, e.g. a pool.

Public API

from threetears.agent.audit import AuditEvent, publish_audit
  • AuditEvent -- pydantic BaseModel with extra='forbid', timezone-aware timestamp validator, closed event_type string family (dotted verb, e.g. workspace.fs_write, rbac.assignment.create). All common identity fields are typed columns on the envelope; event-type-specific extras live in details: dict[str, Any].
  • publish_audit(event, nats_client, namespace) -- fire-and-forget async helper. Serializes the envelope via model_dump_json() and awaits one nats_client.publish on {namespace}.audit.{event_type}. On any publish failure logs at WARN and returns; never raises.

Erasure: anonymize, never delete

from threetears.agent.audit import anonymize_details, anonymize_ip

row.details = anonymize_details(row.details, event_type=row.event_type)
row.ip_address = anonymize_ip(row.ip_address)

An audit record is never deleted by erasure and no id on it changes. Every erasure path (a GDPR request, a principal anonymization, a customer offboarding) routes the record's content through this one rule instead of writing its own:

  • anonymize_details(details, *, event_type) keeps every key of details, keeps the value under a safe key (a dict inside it is judged key by key), and replaces the WHOLE value under any other key with ANONYMIZED_MARKER ("[anonymized]") -- a subtree and the keys a user chose inside it alike. None stays None. Pure and idempotent.
  • anonymize_ip(value) -> str | None returns None: an address is removed, not truncated. It is typed as the column, so its result assigns back without an ignore.
  • SAFE_DETAIL_KEYS is the explicit safe list. A key not on it is masked, so a field nobody classified fails safe. PERSONAL_DETAIL_KEYS records the keys classified as able to carry personal data.
  • declare_safe_detail_keys(event_type_prefix, keys) widens the safe set for one event family, and safe_detail_keys_for(event_type) is the single lookup. A declaration is visible only in the process that makes it, so a family whose events the hub erases from the platform audit table is declared in threetears/agent/audit/anonymize.py itself.
  • is_classified_detail_key(key, *, event_type) answers whether a key was classified. The gate that holds producers to the classification judges by this same function, injected because the gate cannot import this package: the threetears.enforcement.audit_details domain, which every producing repo runs over its own src/ with safe_keys_for=safe_detail_keys_for, is_classified=is_classified_detail_key, and 3tears runs it as tests/enforcement/test_audit_details_keys_are_classified.py.

An agent erasing a person: the hub's copy of its audit rows

from threetears.agent.audit import request_audit_anonymization

result = await request_audit_anonymization(
    nats_client,
    identity_token=current_identity_token(),
    agent_id=my_agent_id,
    actor_user_ids=[respondent_admission_user_id],
)
# result.rows_matched, result.rows_changed

The hub holds the audit rows an agent's events became. This asks it to anonymize the ones that agent published about those actors, on {ns}.hub.audit.anonymize (Subjects.hub_audit_anonymize()), with the same rule as above: rows kept, ids kept, details and ip_address anonymized. The hub takes the agent from the verified identity token and touches only rows whose agent is the caller. A refusal (INVALID_REQUEST, IDENTITY_UNVERIFIED, AGENT_MISMATCH) raises AuditAnonymizeRefusedError with the hub's error_code, and retrying meets it again -- including a refusal that carries no correlation id, since a hub that could not decode the body had none to echo. No token, a timeout, a reply that does not decode or carries another request's correlation id, and the hub's ANONYMIZE_FAILED raise AuditAnonymizeUnavailableError, which is safe to retry. The contract, including every obligation of the hub's responder, is the docstring of threetears/agent/audit/erasure.py.

Design commitments

  • Fire-and-forget. Audit publish failures must never break the producing call. The helper catches every exception, logs at WARN, and returns.
  • Typed wire contract. extra='forbid' + timezone-aware validator catch publisher-side drift at construction time, not at the consumer's decode.
  • No domain-specific envelope types. Every domain publishes the same model; event_type conveys the domain.
  • No dual-emit. There is no legacy envelope the consumer still accepts in parallel. Emission sites migrate in the same PR that deletes the legacy envelope module.

Subject naming

{namespace}.audit.{event_type} where event_type is the dotted event name (e.g. {namespace}.audit.workspace.fs_write). The consumer subscribes to {namespace}.audit.> so new event types route automatically without consumer-side changes.

Metadata

Release files for 3tears-agent-audit 0.55.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for 3tears-agent-audit 0.55.0
File Size Uploaded
3tears_agent_audit-0.55.0.tar.gz 49.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for 3tears-agent-audit 0.55.0
File Interpreter ABI Platform
3tears_agent_audit-0.55.0-py3-none-any.whl Python 3 none any Details

Total release size: 84.2 kB

Release files / 3tears_agent_audit-0.55.0.tar.gz

Download URL 3tears_agent_audit-0.55.0.tar.gz
Size 49.8 kB
Tags Source
SHA-256 checksum
How to use checksums
ccd87e4b1de863a7010e984f96a8e9fe4ea3dba816bea3f4e3f3c538b868344a
BLAKE2b-256 checksum
How to use checksums
6022652ec1fbadbbcbfa356f85aef434197f2921c1a5c32158004116a44ee2e0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release files / 3tears_agent_audit-0.55.0-py3-none-any.whl

Download URL 3tears_agent_audit-0.55.0-py3-none-any.whl
Size 34.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
94a8aa552dadf6c56fd1bc89736e8d13dc6b08022c938dbd95119180760cf1a6
BLAKE2b-256 checksum
How to use checksums
1393e0e6a7561bc11d002896c12e1981fcc57cb0b9d1fc39637dbc69fff58bdb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release history Release notifications | RSS feed

0.69.0

2 release files

0.68.0

2 release files

0.67.0

2 release files

0.66.0

2 release files

0.57.0

2 release files

0.56.3

2 release files

0.56.2

2 release files

0.56.1

2 release files

0.56.0

2 release files

This release

0.55.0 This release

2 release files

0.54.0

2 release files

0.53.0

2 release files

0.52.1

2 release files

0.52.0

2 release files

0.51.1

2 release files

0.51.0

2 release files

0.50.0

2 release files

0.49.0

2 release files

0.48.0

2 release files

0.47.1

2 release files

0.47.0

2 release files

0.46.1

2 release files

0.46.0

2 release files

0.45.1

2 release files

0.45.0

2 release files

0.44.0

2 release files

0.43.0

2 release files

0.42.0

2 release files

0.41.4

2 release files

0.41.3

2 release files

0.41.2

2 release files

0.41.1

2 release files

0.41.0

2 release files

0.40.0

2 release files

0.39.0

2 release files

0.38.0

2 release files

0.37.0

2 release files

0.30.0

2 release files

0.29.0

2 release files

0.28.0

2 release files

0.27.0

2 release files

0.26.1

2 release files

0.26.0

2 release files

0.25.0

2 release files

0.24.7

2 release files

0.24.6

2 release files

0.24.5

2 release files

0.24.4

2 release files

0.24.3

2 release files

0.24.2

2 release files

0.24.1

2 release files

0.24.0

2 release files

0.23.9

2 release files

0.22.4

2 release files

0.22.3

2 release files

0.22.2

2 release files

0.22.1

2 release files

0.22.0

2 release files

0.21.0

2 release files

0.20.0

2 release files

0.19.4

2 release files

0.19.3

2 release files

0.19.2

2 release files

0.19.1

2 release files

0.19.0

2 release files

0.18.0

2 release files

0.17.9

2 release files

0.17.8

2 release files

0.17.7

2 release files

0.17.6

2 release files

0.17.5

2 release files

0.17.4

2 release files

0.17.3

2 release files

0.17.2

2 release files

0.17.1

2 release files

0.17.0

2 release files

0.16.1

2 release files

0.16.0

2 release files

0.15.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page