Skip to content
anecticoDocsDashboard
Browse documentation
Reference

Credential revocation and audit behavior

Check revocation timing, streaming cutoffs, export ownership, audit limits, and delegation history.

On this page

For a single credential, start with Revoke an agent's access.

Find the key first, if you only know the agent

Revocation acts on a key id. If what you have is an agent — a name in the inventory, or a record Anectico discovered for you — ask which credential produced its telemetry. In the product this is Govern → Inventory → Attribution on the agent's row (See every agent that is running); over the API it is:

GET /api/v1/assets/{assetId}/attribution

Every mapping in the response carries a credentials list naming the API keys seen producing that agent's telemetry, with a first- and last-seen window and a sighting count, so a key used once and a key on today's hot path are distinguishable before you act. Feed the api_key_id you find to the revoke command below.

Two answers there are not keys, and both are useful rather than dead ends. bearer_token means a signed-in person's session produced the telemetry — there is no key to revoke, and the control is that person's access. dev_org_header means a development-only trust setting accepted it with no credential at all; nothing can revoke that, and seeing it outside development means the setting is on where it should not be.

An empty list means nothing was recorded for that agent, which is not the same as "it used no credential" — see the attribution response.

Revoke

anectico apikey list
anectico apikey revoke <key-id> --yes

anectico apikey list shows every key's id, name, status, project binding, expiry and last used — which is how you find the right one when several agents share a workspace. The SCOPES column is truncated to keep the table readable; audit a key's real grant with anectico apikey list -o json.

In the product, the same thing lives under Settings → Access.

Revocation is permanent. A revoked key cannot be un-revoked; replacing it means minting a new one. Revoking requires api_key:write, so it is not something an investigating agent's own key can do to itself or to another.

What stops immediately

Every new request. Anectico re-checks the key against its authoritative record on every request — there is no cached credential and nothing to expire out of a cache — so a revoked key starts failing at once, everywhere, across the MCP endpoint, the REST API and the CLI alike.

What the agent sees is a plain 401 with the generic body {"error":"unauthorized","message":"authentication required"}. It is deliberately the same answer a malformed, unknown or expired key gets: the endpoint will not confirm that a key it rejects ever existed, so the response cannot be used to probe which credentials are real. That means the 401 alone will not tell you why — check the key's status with anectico apikey list rather than reading the failure.

Every outstanding confirm token. A preview's confirm_token is bound to the credential that previewed. Once that credential cannot authenticate, the second call never runs, so nothing previewed and not yet applied can be applied afterwards. (The token would have expired within 10 minutes regardless.)

Every idempotency replay. The ~10-minute window in which a retried call replays its first result is keyed to the same credential and is equally unreachable.

What finishes rather than stopping

Be exact here rather than reassuring.

A short request that has already authenticated is not torn down. Revocation is a decision about future authentication. A request that got past the door before you revoked keeps the authorization it was admitted with, and that authorization is not re-checked against the revocation while the request runs. It is not a promise that the request succeeds — it can still fail, time out, or be cancelled — only that revoking is not what stops it.

Anything still delivering data when you revoke is the exception, and it does stop. Three things can still be sending after minutes: a download of an export's contents; a live stream — the log tail behind Explore → Logs → Live, anectico logs tail, and /api/v1/stream/logs, plus a live investigation; and an A2A streaming call, either a SendStreamingMessage still working through an investigation or a SubscribeToTask following one. All of them re-check the credential while they run instead of re-reading the authorization they began with. For a key that means its current key record; for a signed-in person it means that exact sign-in session plus the person's current membership. Revoking either stops them:

Still running when you revoke It stops within
An export download a few seconds
A live log tail or investigation stream 30 seconds
An A2A SendStreamingMessage or SubscribeToTask 30 seconds of its next update

Narrowing a key's scopes stops all three the same way, and so does freezing the key. A role downgrade does the same to a signed-in person's stream when it removes anything the stream was admitted with.

SubscribeToTask is the one worth naming separately, because it is the only call here with no natural end: it opens with the task's current snapshot and then follows newly committed updates for as long as the caller stays connected. It is bounded by the same 30 seconds as everything else.

The "of its next update" in that row is exact rather than a hedge. An A2A stream is re-checked when it has something to hand over, so a stream sitting idle mid-investigation is not interrupted the moment you revoke — but it also delivers nothing while it sits there, and the next thing it would deliver is refused instead. Nothing produced more than 30 seconds after the revocation reaches the caller.

What the client sees is deliberately blunt. A download ends as a file shorter than the length it was promised, which every HTTP client treats as a failed download. A stream ends with a final event naming the reason — unauthorized when the credential no longer authenticates, forbidden when it no longer holds the permission the stream needs — and marked as final, so a client stops rather than reconnecting against a dead key. Once a response has started there is no way to turn it back into a refusal, and a truncated answer is better than a complete one the caller may no longer have.

An A2A stream says the same three things in its own protocol, as a final JSON-RPC error in place of the next update: UNAUTHENTICATED when the key no longer authenticates, UNAUTHORIZED when it still authenticates but no longer holds what the stream was admitted with, and a server error when the key could not be checked at all. Only the first is ambiguous between "your credential expired" and "your credential was taken away" — A2A has one code for both — so the message says which, and an agent holding a revoked key is told to obtain a new credential rather than to retry with that one.

A signed-in person's live stream has the same bound. Each sign-in is a separate session, so signing out one device stops that device's live work without killing the person's other signed-in devices. Removing the person's membership, expiring that session, or signing it out is final for that stream. A role downgrade is checked against the current membership rather than the role copied into the authorization at sign-in.

An authorization merely reaching its routine expiry is different: the session can still be live, and the browser can renew it. In that case the stream ends without being marked final, the dashboard signs the person back in, and it resumes where it left off. An A2A caller is told to re-authenticate and call again. A session that was signed out or removed is marked final instead; presenting the same session again cannot repair it.

An export already commissioned still gets produced. An export job records the scopes it needs at creation time and runs on its own schedule, so revoking the key does not stop the file being written. Downloading it is a different question, and the answer is better: the download address is an Anectico API path that re-authorizes on every request rather than a link carrying its own authority, so possessing the address grants nothing, the revoked key cannot fetch the result, and a fetch already in progress is cut off rather than allowed to finish. Because a key-created job records the key as its creator, after revocation only an organization owner or admin can download it. Completed results remain downloadable for 24 hours.

If a running export is itself the thing you want stopped, cancel it before you revoke — cancel_export_job works on a pending, running or retrying job, and a canceled job cannot be resumed.

What revocation cannot take back

Anything that already left Anectico. An action with effect_scope: external caused something outside your workspace that no credential change retracts: a ticket filed or closed in your tracker, an invitation emailed, a test message delivered to a real channel, an incident that paged every enabled channel. Ask the server for the complete current list rather than trusting a fixed one here:

{"name": "list_write_actions", "arguments": {"effect_scope": "external"}}

Anything already deleted. delete_log and delete_trace are durable deletions. Nothing restores them, and neither does revoking the key that ran them.

Anything already read. Data the agent retrieved is in its context, its transcript, and possibly its host's logs. Revocation ends future access; it does not un-read a person's profile properties or an agent-run transcript. If the concern is what was read rather than what was changed, the control that mattered was the scope the key was minted with — see Scope recipes for agent jobs.

Anything a notification already carried. A message delivered to Slack, an email, or a webhook went to a destination with no permissions of its own. Narrowing or removing a credential does not change what was delivered; change the alert rule that feeds the destination.

Review what the agent did

Read the trail under Settings → Audit, which needs audit:read — a scope owners and admins hold. Filter by resource, by action, or by result.

Every mutation that reached its handler is there, and not only the ones that worked. Each record's result is one of:

Result What it means Did anything change?
success The change was applied Yes
denied Refused — scope, confirmation, or policy No
previewed A confirm-gated call returned a preview and stopped No
throttled The write budget refused the call No
error The call reached the change and failed, or ended by an unknown path Unknown — cross-check
unknown The platform could not classify the outcome Unknown — cross-check

Alongside the result each record carries the resource, the action, the tool name, and the affected object. Filtering on denied is usually where a review starts: a denied beside a previewed beside a throttled is the shape of an agent trying things it was not scoped for.

unknown never means success. It means the platform declined to guess, which it does rather than record an outcome it cannot vouch for — an empty list of refusals must mean there were none, not that the question could not be answered.

Who gave this agent its access, and when

Reviewing what an agent did is half the question. The other half is what it was ever allowed to do, and who decided that:

GET /api/v1/account/audit-events?target_api_key_id={id}

That returns the key's whole grant history and nothing else — issued, scopes changed, revoked, newest first. It matches the key that was changed, never the key that did the changing, so a key used to administer other keys does not collect their history.

Every one of those records carries the scope set in metadata:

  • scopes — what stands after the act. On the issue it is what was granted; on the revocation it is what the credential could do at the moment it stopped working, which nothing can tell you afterwards. An empty value means nothing was granted, which is different from the field being absent.
  • previous_scopes — the set a change replaced, present only on a scope change. A rename replaces nothing and carries neither, so a record with both is always a real widening or narrowing.

event_type narrows it further — api_key_created, api_key_updated, api_key_revoked — and composes with the key filter, so "every time this agent's scopes changed" is one request.

The key listing itself answers who issued the credential: user_id for a person, created_by_api_key_id when a key issued a key, and created_by_state saying which of those applies so an empty user_id is never ambiguous. orphaned_user there means a person issued it and their record is no longer available — the attribution was lost, and the record says so rather than looking like a key nobody issued.

Revocations always name their actor. A key revoked because its project or organization was deleted records the platform as the actor and names the deletion under metadata.via, so a credential that stopped working without anybody touching it is explained rather than mysterious.

Two things the trail will not tell you, so plan the review around them:

  • A call refused before it was dispatched is not in it. An action the key was never scoped for, or one excluded by the profile the agent connected with, is refused without running, and there is nothing to record. Absence of a record is not evidence the agent never asked.
  • The record is written beside the change, not inside it. A record that fails to persist does not undo the change it describes. For anything consequential, cross-check.

The cross-check is the receipts the agent itself received. Every committed write returned the operation, the outcome, what changed, the targets the platform confirmed, and both effect_scope and destructive — so a transcript says how far each change reached without re-reading the catalog. A receipt also carries a mutation id and a commit time when the platform reported them, and leaves them null when it did not; a null there means "re-read to find out", never "nothing happened".

Rotate instead, when nothing is wrong

If the key is simply old or over-scoped, replace before you revoke so the agent never loses its footing:

anectico agent bootstrap --project <project-id> --host claude --scope-profile investigate
eval "$(anectico agent key env --export)"
anectico apikey revoke <old-key-id> --yes

Re-bootstrapping the same profile replaces the stored secret and the anectico entry in the host's MCP configuration rather than accumulating a second one; every other entry in that file is written back untouched.

Two habits make this rarely urgent:

  • Give every key an expiry. anectico agent bootstrap defaults to 30 days, and --expires-in accepts anything up to 365 days. An expired key fails exactly like a revoked one.
  • One key per purpose. A developer's local agent, CI, a shared support workflow and production operations should not share a credential — otherwise revoking one of them takes down all four, and the audit trail cannot tell them apart.

Withdraw a delegation, without touching the credential

Revoking a key stops a credential. Withdrawing a delegation records that one authority relationship should no longer exist — "this agent may no longer act through that tool for that purpose" — and leaves the key alone.

The two are different acts and you often want both, in that order. A delegation is identified by an edge id from the authority graph:

GET  /api/v1/authority-edges?from_principal={principal}
POST /api/v1/authority-edges/{edgeId}/revoke

The POST body carries one field, reason, and it is required — a withdrawal that records who and when and nothing about why is the one a reviewer six months later cannot act on. Requires agents:write. Who withdrew it is taken from your credential and cannot be set in the body.

Four things are worth knowing before you rely on it:

  • It is not a delete. The edge stays, with its full sighting history — how often the delegation was exercised and when it last was. Withdrawing authority is not a claim that it was never used.
  • It disappears from the default read. GET /api/v1/authority-edges hides withdrawn delegations, because the question that read answers is "what authority exists now". Pass include_revoked=true to see the history.
  • Repeating the call is safe. The response says already_revoked: true and returns the withdrawal that stands — the first reason and the first instant, not yours.
  • Using a withdrawn delegation does not restore it. If an action later exercises it anyway, the delegation stays withdrawn and a review signal is raised for a human, visible at GET /api/v1/review-signals?status=open. A withdrawal cannot be undone by ignoring it. This records the violation; it does not stop the action — for that, revoke the credential or contain the agent.

What is not a revocation

A profile is not a revocation. Connecting with ?profile=investigate really does stop that connection invoking a mutation — but the caller chose the parameter, the key still holds every scope it was minted with, and the next connection without it is served all of them again. Only revoking or re-scoping the key changes what the credential can do.

Withdrawing a delegation is not a revocation of the credential. It records that an authority relationship should not exist and makes any further use of it visible; the key still works and the agent can still act. Do both when you mean both.

Removing the MCP server from a host config is not a revocation. The secret is still valid wherever else it has been copied — the environment variable, a CI secret store, a shell history. Revoke the key.