Concepts
Audit Logs
Credential access, policy decisions, provider calls, and lifecycle events with identity and correlation context.
An audit log is the canonical record of what Alter did and why. Credential retrievals, policy decisions, grant lifecycle events, provider calls, and administrative actions are written with the available identity and correlation context needed for compliance and forensic review.
Audit logging is always enabled; there is no disable flag. Credential-touching backend paths require an explicit success, failure, denial, or error audit terminal. Direct SDK provider calls queue their audit report after the provider response. Audit-write failures are isolated from the primary operation and emit critical diagnostics and failure metrics rather than replacing the operation’s result.
What’s recorded
Section titled “What’s recorded”For every app.request():
- Caller identity — the API key, agent identity, and
callerattribution. - Principal identity — the user, group, system, or agent the grant belongs to.
- Grant — the
grant_id, provider, and grant kind. - Request — method, URL, response status, latency, and available size metadata.
- Context — application-supplied metadata: tool name, run ID, thread ID, reason.
- Outcome — success, policy denial, provider error, network error.
- Deciding policy rule — every policy decision names the rule that decided it: the rule that denied the call (including an IP allowlist the caller’s address is outside, an approval or step-up requirement, and every other rule that also denied), the rule that required an approval, or the rules that shaped a permitted call: obligations (approval, step-up, redaction) and restrictions it satisfied, such as an IP allowlist (
decision_basisnarrowed). A call no rule restricted records an explicitdecision_basisofno_restricting_rulerather than an invented rule.
Sensitive headers (Authorization, Cookie, AWS signing headers) are stripped before storage. The audit record stores call metadata — method, URL, status, latency, and request/response sizes when available — never the request or response body. Capturing request and response payloads is a separate, opt-in feature; see Request and response payload capture below.
For every grant lifecycle event:
- Created, revoked, deleted, scope-drifted, policy-violated.
- Who initiated the event (end user via Wallet, operator via portal, SDK call).
- When an agent is granted access, or a delegation’s terms change: the delegation terms — the requested and applied scope constraint, whether the delegate may re-delegate, and the expiry. An update additionally records the previous terms, so the trail shows whether a delegation was narrowed, widened, or otherwise modified without reconstructing it from current grant state.
- Every delegation event — including revocations, and the cascading teardowns that follow when a parent grant, group membership, secret, agent, or user is removed — identifies the delegation edge itself: the recipient agent (ID and name, snapshotted at write time so the record survives later agent deletion) and the delegated child grant alongside its parent grant. The event answers “which agent received — or lost — this access?” on its own, without correlating against current grant state. A cascade event describes one bulk teardown, so it lists the affected delegations along with the total number revoked; very large teardowns list a capped subset and state how many were not listed, so the recorded set is never mistaken for the complete one.
For every administrative action:
- Provider configuration changes, API key mint/rotate/revoke, agent create/update/revoke, identity provider configuration.
- Policy-rule changes, whoever makes them: operators in the portal or CLI, and end users who create, update, or delete their own policies in the Wallet. Each row names the changed rule’s id, so a rule is findable by id across both authoring surfaces.
- Read-only administrative access, including grant/policy listings, captured-payload views, audit exports, and integrity checks. These rows are classified as Read and their count/offset payload is labeled Read Summary; it is never presented as a created resource or state change.
Successful administrative rows carry an effect classification: Read for data access and Write for state changes. An unsuccessful outcome overrides that intended effect and is classified as Error. An unknown successful action defaults to Write so a newly added mutation cannot disappear from compliance review.
For every user-provisioning event (the Provisioning tab in the portal’s Audit Logs view):
- Users and groups created, updated, or deprovisioned — whether by an IDP webhook, a directory sync run, or a first sign-in.
- Group membership transitions that change access: a member removed (group-grant access revoked) and a membership restored after the member reappears in the directory.
- Each row records the originating path, the outcome (success, no-op, skipped, denied, error), and the reason.
- A deprovisioning records a causation id on the triggering provisioning event (with the IDP event id or directory import run) and on every grant revocation, delegation revocation, and managed-secret revocation it caused, so the full consequence of one deprovisioning is retrievable by that id rather than by timing: searching an app’s audit log for the causation id returns exactly the rows that deprovisioning wrote.
Request and response payload capture
Section titled “Request and response payload capture”By default, Alter records call metadata in the audit trail — never the request or response body. Capturing request and response payloads is a separate feature that is off by default and turned on per app by an admin in the developer portal under Settings → Payload Capture. Capture can be narrowed to specific agents or end users.
Capture applies wherever the audit trail sees the bodies:
- ✅
app.proxy_request()— backend-mediated execution; the backend captures the bodies it handled. - ✅ Calls against human-in-the-loop approval grants, which execute on the backend after approval.
- ✅
app.request()— the SDK executes the call itself and reports it to the audit trail; when capture is enabled for the app, the reported request and response bodies are captured from that report. - ✅
app.boto3_client()(Python SDK only) — every AWS call made through the returned client is routed through the same SDK execution path asapp.request(); per-call request and response bodies are reported and captured identically.
Narrowing by end user behaves differently on the two execution paths, deliberately. A backend-executed call is matched against both end-user identities it knows — the grant’s owner and the calling end user. A call the SDK executes and reports is matched against the grant owner only: the caller identity on that report is client-asserted, and an unverified claim is never allowed to widen what a compliance surface records. So a capture list narrowed to a caller-side end user records nothing for SDK-executed calls; narrow by agent, or by the grant owner, to cover both paths.
What is and isn’t stored:
- The caller-supplied request body and the provider response body are captured. Both are size-capped, and an incomplete capture always says so: the stored payload carries a truncation flag and the original size on both execution paths. Calls the SDK executes and reports are capped by the SDK before it reports them — an oversize body is replaced by a placeholder noting its size, and a body that is not valid UTF-8 is replaced by a placeholder whatever its size — and that upstream truncation is recorded on the stored payload rather than being mistaken for a complete body. A captured body from that path is evidence of what was sent, not always a byte-exact copy.
- The injected credential is never captured. The request body is recorded before Alter injects the credential, so a stored payload never contains the secret.
- Captured payloads are a diagnostic tier, not part of the permanent audit record: the default retention window is 30 days, after which they are deleted. They are never included in audit exports.
- Every captured payload records where its bytes came from, and the portal labels it: proxy-captured means Alter observed the bytes itself (it sent the request and received the response); client-reported means the SDK executed the call and reported the bodies, so they are the application’s own account of the call rather than something Alter witnessed. The two are never presented interchangeably.
Captured bodies are visible to app admins from the audit log detail view in the developer portal.
The two questions an audit trail answers
Section titled “The two questions an audit trail answers”Alter’s audit model is built around two questions that traditional credential systems struggle with:
- “What was Alice’s data used for?” — calls carrying Alice’s app-scoped principal can be filtered by end-user identifier and followed through related keys, grants, and traces.
- “What did Agent X access?” — calls attributed to Agent X can be examined in the app’s grouped-principal and trace views.
The developer portal exposes end-user, managed-agent, originally-granted-to, provider, action, policy-decision, and time filters, plus key, trace, and thread drill-downs. The CLI supports app, actor, root-grantee, provider, action, policy-decision, and time filters with principal, trace, and thread drill-down commands.
Who the access was originally granted to
Section titled “Who the access was originally granted to”When a user delegates access to an agent, and that agent shares it onward, each call is made by the agent at the end of the chain, but the access traces back to the user who granted it first: the root grantee. When a user, group or agent used its own grant directly, the root grantee is that user, group or agent.
The Originally granted to filter (on the portal’s Access & Requests and All Activity tabs; alter audit list --root-grantee <id> in the CLI) narrows the audit trail, and an export, by root grantee. Pick the user, group or agent (deprovisioned users and revoked agents included), or paste its ID.
Today the search is partial. Every filtered response says so in its root_grantee_coverage field, and a filtered export carries the same value in a response header. The value is direct_access_only, and the portal and the CLI show what it means:
No audit record carries a root grantee yet, so this result has only token and managed-secret records of the user’s, group’s or agent’s direct use of its own grant: retrievals, refreshes made during a request, failures, and refusals of a request to use the grant once it was identified. Access through a delegation, background (refresh-ahead) refreshes, provider calls, grant lifecycle events, approvals and attempts refused before the grant was identified are not included.
Existing records are never filled in later, so an empty result does not mean the access was never used. A response without a root_grantee_coverage value comes from a backend that predates the filter, so its rows are the unfiltered trail. In that case alter audit list --root-grantee prints no rows, says on stderr that the filter was not applied, and exits 1. The portal shows an error in place of the results instead of the unfiltered rows, and an export downloads nothing and says the filter was not applied. A root_grantee_coverage value the portal or the CLI does not recognize still marks the result as partial, so both show a general partial-result notice naming the value.
Who can see what
Section titled “Who can see what”The trail has two planes, and they answer to different roles.
Runtime rows — credential retrievals, provider calls, policy decisions, grant lifecycle, approvals, identity assertions, policy changes end users make in the Wallet, and the trace / thread / agent views built on them — are organization-scoped and readable by every member of the organization. Members build the agents that produce these rows, so they need them to debug their own work.
Management rows — who created or revoked an API key, changed an identity provider, edited a policy, or archived an app, together with the acting operator’s identity and the before/after values — are the record used to detect insider misuse. They are scoped to organization admins. A non-admin member sees only the management rows they produced themselves, which keeps their own history available without exposing anyone else’s.
This matches how comparable platforms treat an administrative audit log: GitHub restricts the organization audit log to owners, Okta’s System Log to admin roles, and Slack’s audit API to org owners, while everyday request logs stay broadly readable.
A narrowed view always announces itself — the portal shows a notice above the
feed, the CLI prints one to stderr, and a narrowed export carries an
X-Audit-Management-Visibility header — so a short result set is never
mistaken for a quiet month.
Two surfaces are admin-only outright rather than narrowed, because they produce or verify the organization’s compliance evidence rather than showing a view of it: bulk export (see below) and the integrity check that re-verifies each record’s tamper-evident hashes: its content hash and, on credential records that carry one, a lineage hash over the attribution fields the content hash does not cover (the access path, delegation chain, grantee and root grantee). Placing a legal hold and revealing a captured payload are admin-only for the same reason.
Where to read the audit trail
Section titled “Where to read the audit trail”- Developer portal — every app has an Audit Logs view with filtering, search, and export.
- End-user Wallet — each end user sees the app-scoped slice of the audit trail concerning their own grants, including the application’s
reasonwhen supplied. Wallet users are not organization members and never see the management plane. - Export — see Audit log export for downloads, CLI ingestion, telemetry mirroring, and SIEM delivery.
What’s next
Section titled “What’s next”- Audit log export — sending audit logs to an external system.
- Policies — denials are first-class audit events.
- Wallet — what end users see.