Skip to content
anecticoDocsDashboard
Browse documentation
Reference

Permission scopes

Choose the least API-key permissions required for ingestion, investigation, response, and administration.

On this page

API-key scopes use resource:action. A key can receive only known scopes held by the member creating it. Project scope and permission scope are both enforced: neither can widen the other.

The authenticated scope catalog at GET /api/v1/account/api-keys/scopes and Settings → Access is authoritative. The groups below describe the current selectable catalog.

Telemetry and investigation

Resource Scopes
Ingestion ingest:write
Traces traces:read, traces:delete
Logs logs:read, logs:delete
Metrics metrics:read, metrics:delete
Service health services:read
Errors and Issues errors:read, errors:write, errors:delete
Query and search query:read, query:write, search:read
Saved searches saved_search:read, saved_search:write, saved_search:delete
Customers and accounts persons:read, persons:profile:read, groups:read, groups:write, groups:delete
AI agents and LLM cost agents:read, agents:content:read, agents:write, llm:read
Events, cohorts, and flags analytics:read, analytics:query, analytics:write, cohorts:write, cohorts:delete, flags:read, flags:write, flags:delete
Tracking-plan configuration tracking_plans:read, tracking_plans:write, tracking_plans:delete
Saved-insight definitions insights:read, insights:write, insights:delete
Anomalies anomalies:read, anomalies:write, anomalies:delete
Replay replay:read, replay:content:read, replay:write, replay:delete
Releases releases:read, releases:write
Export export:read, export:write, export:download (owner/admin only)

errors:write allows Issue lifecycle actions and symbol uploads; deleting error data requires errors:delete, which covers both units it has — deleting one Issue with its occurrences, and erasing one customer's error occurrences across every identity they have used. Both are project-exact and permanent, including for occurrences that arrive afterwards. errors:delete is not granted by any of the narrower API-key presets (read-only, read-ingest, ingest-only) and must be named explicitly — only the Full access preset, which grants the complete scope catalog, includes it. Application SDKs use ingest:write for OTLP telemetry and captured errors, plus analytics:write when sending diagnostic, identify, group, or screen events through the capture surface. Session replay uses replay:write instead of ingest:write.

Tracking-plan list/get operations are available through REST, MCP and CLI; create/update/delete use confirmed MCP actions and CLI, with session-only REST for human clients. See Manage tracking plans. tracking_plans:read is viewer-granted; tracking_plans:write is member-granted; tracking_plans:delete is owner/admin-only. Writes and deletion also require the read scope for configuration receipts. Capture and settings permissions do not grant plan management. OAuth-delegated agents receive the read scope only; writes require a deliberately scoped API key. Tracking permissions do not grant access to captured event values or change content permissions.

analytics:query authorizes typed analytics execution separately from reading events or results. It also requires analytics:read and every source permission used by the definition. It is granted to organization roles including viewers and is eligible for delegated OAuth; it does not grant cohort mutation, export, or access to otherwise restricted source content.

Saved-insight get/list/search require insights:read. MCP and CLI create/update also require insights:write; retirement requires admin-granted insights:delete. All mutation operations require both mcp:read and mcp:write, preview and exact-argument confirmation. API keys can retire only their own definitions; current admin user sessions can retire project definitions. These permissions do not grant telemetry access. See Manage saved insights.

Derived audience creation/replacement additionally requires analytics:read, persons:read and all inherited source permissions alongside cohorts:write. MCP and CLI mutations use both mcp:read and mcp:write, preview/confirmation and a stable UUID key. Exact audience reads need analytics:read, persons:read, inherited source permissions and mcp:read, but no write or query scope. See derived audiences.

Creating, updating, refreshing, and replacing ordinary cohort membership requires cohorts:write. This member-granted scope is separate from SDK capture (analytics:write) and is excluded from external OAuth delegation. Application and browser SDK key presets do not grant cohort management. Deleting one is deliberately separate under cohorts:delete, an owner/admin-only scope, because deletion is destructive.

services:read names per-service request rate, error rate, and duration, plus the per-operation breakdown. agents:read names AI agent-run summaries: which agent ran, its model, step and tool-call counts, duration, token and cost totals, and outcome. It also names the server-owned 24-hour fleet summary and the agent inventory and evidence trail around it — declared and telemetry-discovered agents, the review queue over those discoveries, which telemetry belongs to which agent, step-level agent events, action receipts, chain verification, and signed bills of materials. None of that admits prompt, completion or tool content: agent events expose bounded identity and provenance metadata, never content or a content digest. It also reads exact outbound-drive audit records and recorded-session replay results. Replay counts, stop reasons, comparison kinds, sizes, and digests remain visible; the real user turns and agent responses still require agents:content:read. llm:read names LLM spend — the per-model summary, the cost series, and the per-customer spend leaderboard.

agents:write is the one write in that family, and it is separate from every read in it. It names declaring what you have deployed, the configuration that deployment resolved, the bill of materials that configuration is made of — the three facts that let Anectico tell you when a running agent has drifted away from the composition it was approved with — and what your agent actually did: one lifecycle step of an action it took in the world, appended to your tamper-evident action log. It also authorizes the explicit outbound control actions: opting one registered agent into driving, sending one caller-authored message, and explicitly replaying one real recorded session. Starting a session replay additionally requires agents:content:read because the action exports the person's recorded words. Because these calls can trigger another system and spend its money, agents:write is checked even when a trusted internal caller performs the transport work.

Nothing here can be inferred from telemetry. Your spans carry a configuration hash, and only the system that resolved the configuration can say what that hash stands for; and only your agent knows that it intended an action, was refused one, or could not confirm that an effect landed. So a release pipeline declares its inventory and your agent records its own conduct, both with a key holding agents:write and no dashboard involved. Reading any of it back — a configuration, a manifest, a receipt — is agents:read, not this scope, and so is asking for a signed checkpoint of your action log. It is owner/admin-only to mint, because a declaration changes what drift detection watches.

The scope labels what you declare; it never lets you declare it as ours. Every artifact it produces records whose claim it is, inside the bytes the artifact's own digest covers:

You declare Recorded as Anectico's own is
A bill of materials authority: "declared" authority: "mediated"
An action-receipt step receipt_authority: "reported" receipt_authority: "mediated"

declared means Anectico attests to what you declared and when, not that the components are accurate. reported means Anectico attests that it received this statement and when, not that the action happened. mediated is reserved for what the platform itself composed or performed. You cannot choose the value — it follows from the credential that made the call, and a request that sets one is rejected. Because it is inside the signed or chained bytes, anyone you hand an exported record to can tell the two apart with the published key and no call to us. (The REST authority and receipt_authority response fields carry the same fact as the prefixed enum name — ABOM_AUTHORITY_DECLARED/ABOM_AUTHORITY_MEDIATED and RECEIPT_AUTHORITY_MEDIATED/RECEIPT_AUTHORITY_REPORTED — outside the signed or chained bytes.)

One action has one authority for its whole life. A step you report under an action Anectico performed is rejected rather than merged, so a receipt can never show your statement under our name.

Each of those three reads stands on its own scope. A key carrying only services:read, agents:read, or llm:read reads that summary without also reading the spans underneath — that separation is the point of the split. The same endpoints still accept traces:read, so a key that already holds it keeps working unchanged; scope new keys with the specific read instead of traces:read.

Three further scopes name recorded content rather than a summary of it, each separate from the scope that lists the same records:

Scope Names Its metadata-only counterpart
persons:profile:read A customer's profile properties — the traits your identify() calls attach, such as email, name, and plan persons:read (identity, aliases, activity timeline)
replay:content:read Recorded session content: the replay snapshots and their console and network payloads replay:read (recording list and metadata)
agents:content:read Agent-run content: prompts, completions, tool arguments and results, the reasoning transcript, and exact recorded-session replay turns and responses agents:read (run and replay metadata, comparison, and cost)

Treat all three as personal-data scopes. Replay masking is configured in the SDK and is best-effort, and an agent transcript contains whatever your end user typed plus whatever the agent retrieved for them, so a key that reaches this content reads it in its raw form.

Each content scope is required for the content it names, everywhere. The metadata-only counterpart no longer reaches it, on any surface — not the API, not the dashboard, not the MCP endpoint, not an AI investigation. Concretely:

  • replay:read lists recordings and reads their start, end and duration. It does not open one: playback and the panel of errors and events captured during a session need replay:content:read.
  • agents:read gives a run's summary — agent, model, steps, tools, duration, tokens, cost, outcome. The run's transcript needs agents:content:read.
  • persons:read gives the identity spine — who exists, their aliases, their activity counts and timeline. Their profile properties need persons:profile:read, and so does anywhere those properties are used or matched: without it, an issue's affected customers and the LLM spend leaderboard identify people by their raw identifier instead of their name or email, and the people search matches identifiers only, so you cannot search by email address. (Whether a person is identified is part of the spine and is always shown — hiding it would misreport identified customers as anonymous.)

Some requests refuse outright with 403; others return the record with the restricted field omitted, so the surfaces that carry both — the people list, an issue's affected customers, an agent run's scores — keep working for every role.

In the dashboard, a refusal costs you that panel and nothing else. The three detail pages that carry restricted content are assembled from several reads, and the ones you are allowed keep rendering:

Page Still shown Replaced by a short note
A customer Their identifiers, first and last seen, the counts for events, errors, traces, logs, replay sessions and LLM spend, and the full activity timeline The profile properties panel, and the accounts they belong to
An agent run The run's shape — agent, model, status, steps, tool calls, errors, duration, cost — plus its quality scores and related errors The span timeline, which carries the transcript
A session recording Who the session belongs to, when it started, how long it ran, how many segments it has Playback, and the product events captured during the session

The note names what is hidden and says to ask an organization owner or admin. It never offers a retry: nothing is broken, and the same request would be refused again. An outage looks different — it says so and gives you a Retry.

The same rule follows the content wherever it is retold. An automated quality score's written explanation is produced by a model that read the run's transcript, so it needs agents:content:read; the numeric score and its label do not. An AI investigation is answered from whatever its author was allowed to see, so reopening or continuing one needs the same access its author had — otherwise Anectico shows the investigation and withholds the answer, and you can re-run it to get one built from what you can see.

Alert labels follow it as well, and this one bites in a place people do not expect. An alert rule grouped or filtered on a customer field labels its alerts with that field's value — an alert grouped by user_email carries an email address. alerts:read admits the alert, not the address: the customer-identifying labels (user_email, user_id, user_ip_address, and the customer identifier) are returned only to a key that also holds the read scope of the signal the rule queries, such as errors:read. Two things follow from that:

  • Alerts whose labels are withheld also come back with an empty fingerprint and group_key. Those are digests computed from the full label set, so returning them would let a caller confirm a guessed email address. Alerts that carry no customer-identifying label are unaffected, and grouping and de-duplication behave exactly as before either way.
  • Notifications never carry them. A Slack message, an email or a webhook goes to a destination with no permissions of its own and cannot be recalled, so those labels are left out of the delivered message and out of the stored delivery record — for every recipient, regardless of who configured the channel. Alerts still fire, still de-duplicate and still deep-link; open the alert in Anectico to see which customer it is about. Silences are unaffected: a silence matching on user_email still matches.

Computed answers follow the data they were computed from

Anectico computes things for you — a cohort's membership, the accounts a segment matches, the customer an alert fired on, a feature-flag decision. Each of those is an answer to a question asked of your customer data, so each one needs the scope that admits the data behind it, not just the scope that admits the page it appears on.

A key reading… also needs
Cohort members, member counts or a cohort preview built on a profile property persons:profile:read
Cohort members, member counts or a cohort preview built on events analytics:read; add agents:content:read for event-property filters outside the exact operational-metadata allowlist
An account segment's matched accounts, or flag targeting on account properties groups:read

What you will notice:

  • A flag decision never varies with something the key cannot read. Without the scope behind a cohort or an account property, that targeting condition simply does not match — the same answer an unmatched condition has always given. Give SDK keys that target on cohorts or account properties the matching scope; leave it off keys that do not.
  • A member count reads as 0 when the key lacks the scope, and a member list is refused. A cohort of "everyone whose email is X" has one member or none, so showing the count would be showing the answer. Zero alone therefore does not prove an empty cohort. MCP refresh receipts mark this ambiguous value as unknown and omit the count change; an authorized member-page response can establish the exact count for the returned generation.
  • Per-entity alerts hide which customer they are about from a key without the scope — the id, the label, the deep link and the customer's name in the alert text all go together. The alert itself, its severity and its value are unchanged. Cohort-derived alert identities require analytics:read, agents:content:read and persons:profile:read, because the alert mirror does not carry the precise cohort predicate.

One limit worth knowing: changing a key's scopes changes what it can read, not what has already been sent. The notification you configured still names the customer — you chose that destination deliberately, and a Slack message or email cannot be recalled. If a destination should stop receiving customer identities, change or remove the alert rule that feeds it; narrowing a key's scopes will not do it.

  • What you wrote is still yours to read. Cohort definitions, segment predicates and cohorts you uploaded by hand are not affected — only the computed answer is.

Every role from member upward already holds all three scopes, so nothing changes for people signing in. This binds narrowly scoped API keys, and one role: a viewer can no longer list the members of a cohort built on a profile property.

A signal read scope no longer reaches customer content. It used to. Prompts, completions, tool arguments and retrieved documents recorded as span attributes by third-party AI instrumentation came back verbatim from the raw trace endpoints to any key holding traces:read. They do not any more: those attributes are withheld unless the credential also holds agents:content:read, and the response says so — the span carries an anectico.content.withheld attribute naming exactly which keys were removed, so a missing body is never confused with a call that had none. Everything else on the span is untouched: token counts, model names, latency, status and your own attributes are metadata and stay readable on traces:read alone.

The same is now true of logs and of metric exemplars, and this page previously said the opposite. A log record's attributes and resource maps, and the filtered_attributes on a metric exemplar, carry the identical convention keys and are withheld from a credential without agents:content:read — on log search, on a single log entry, on the surrounding-context window, on correlation, and on the live tail. The same anectico.content.withheld marker names what was removed. So a prompt recorded as a log attribute is now governed by the content tier wherever you put it, and you no longer have to record model content on spans for that to be true.

Discovery and grouping follow the same rule as record reads. Log-field discovery omits a content-classified field together with all of its sampled values. Metric-label discovery omits the classified label; a metric query that filters or groups by that label is refused before reading its values unless the credential holds agents:content:read and the project's current read policy permits the class. Ordinary labels and fields are unchanged.

Two things this deliberately does not withhold. The log message itself stays readable on logs:read: a log line is not automatically a model transcript, and treating every one as such would make ordinary log reading impossible. And an error's extra and tags are still governed by errors:read alone — if you put a prompt there, errors:read reads it.

One route to customer content remains open by design. If you use email addresses as customer identifiers, those identifiers are part of the identity spine and appear wherever identifiers do — on traces, logs, errors and every timeline. No scope withholds them, because they are the join key rather than a field. If a credential must not see them, identify customers by an opaque id.

An AI investigation's question and title are user-authored metadata, not derived content: they are what a person chose to type, so they are shown to anyone who can see the investigation and are governed by that choice rather than by the content tier. Treat a free-text question as visible to every role in the organization.

Scope keys accordingly: give a key the metadata-only counterpart, and add the content scope only when it genuinely needs verbatim customer content.

An AI investigation reads with your key, not with ours

An investigation is an aggregation: it gathers issues, traces, logs, customers, accounts, incidents and LLM spend into one grounded answer. investigations:run grants the ability to ask for one — it does not grant the data the answer is built from.

An investigation includes only evidence the key that requested it could have read directly. Each kind of evidence needs the scope that admits it on its own page:

Evidence in an investigation Also needs
Issue detail, occurrences, blast radius errors:read
Release breakdown for an issue releases:read
Traces and spans traces:read
Log lines around an occurrence logs:read
Cross-signal search results and trace-to-log correlation search:read
Customer identity, timeline and labels persons:read
Account timeline, stats and properties groups:read
LLM spend summary, cost series and leaderboard llm:read
Incident state, timeline and attached signals incidents:read
A customer's session-recording list replay:read
Agent-run summary agents:read (or agents:content:read)
Agent-run steps agents:content:read or traces:read
Recorded content — session playback, transcripts, profile properties the content scopes above

Two things follow:

  • A key missing one of these gets a smaller investigation, not an error. The evidence it cannot read is not gathered — and the answer names what was left out, so a smaller investigation never passes for a complete one.
  • A key that cannot read the SUBJECT is refused. Asking to investigate an issue with a key that has no errors:read returns 403, the same answer the issue endpoint gives.

The answer says what it did not look at. An investigation assembled without some kind of evidence ends with a ## Not examined section naming it. That line is written by Anectico, not by the model, so it is always there — because an answer that quietly skipped your errors must never read as "no errors were found".

The conclusion also carries the same fact as structured evidence_disclosure data: each omission names a closed source family and one of SCOPE, POLICY, UNAVAILABLE, or LIMIT. Use that field for automation; the Markdown sections are for people. A present empty disclosure means nothing was withheld, while a missing disclosure means its producer did not populate the contract. Both forms are generated from the same omission set, so the prose and field cannot drift.

Recorded content is disclosed separately, under ## Withheld as sensitive content. The three content scopes in the table above are asked over and above the permission to read the signal carrying the content, so a credential can hold agents:read and receive an agent run's outcome, latency and cost while its transcript is withheld for want of agents:content:read. That answer looks complete, which is exactly why it gets its own section rather than sharing ## Not examined: the run was examined, and saying otherwise would contradict evidence you can see cited in the same answer. A credential holding the content scope gets the identical investigation it always did.

Nothing changes for people signing in: every role, including viewer, already holds every scope in the table above. This binds narrowly scoped API keys, which is what it is for. If an automation is getting thinner investigations than you expect, give its key the scopes for the evidence you want it to reason over.

Your project's content policy answers a different question, and it binds everyone. Permissions decide who may see recorded content. The content policy decides whether that content may be sent to a model at all — at the judge_transfer boundary — and Customer Detective is one of the two places that boundary is crossed. Content the policy does not permit is left out of the investigation for every caller, however broadly their key is scoped, and the answer ends with a separate ## Withheld by policy section naming what was left out. The two sections are deliberately separate: ## Not examined means your credential could not read something, and re-minting the key changes it; ## Withheld by policy means your project does not allow that content to reach a model, and only a policy change does.

Every project starts with a policy that permits what the product already did, so this changes nothing until somebody edits it. One thing is worth knowing: a content policy belongs to a project, so an investigation that spans your whole organization is asked project by project rather than once. In the organization-wide LLM spend question, each expensive agent run belongs to a single project and that project's policy decides whether the run's verbatim messages may be sent — so one answer can carry the transcripts a permitting project allows and omit those a denying project refuses, naming the omission.

The spend leaderboard is the exception, and for a different reason: it ranks people, a person is not confined to one project, and so there is no single policy to ask about their profile label. Those rows are labeled by customer id. The spend, tokens, calls and cost series are not recorded content and are unaffected.

What the viewer role can see

The three content scopes are the only reads a member holds that a viewer does not — every other difference between the two is a write, a delete, or an administrative action. Because the content scopes now bind everywhere, that difference is real:

A viewer keeps every metric, log, trace and error; dashboards, alerts, incidents and on-call; the customer and account lists, activity timelines and per-signal counts; the recording list; and agent-run summaries and LLM spend.

A viewer cannot play back a session recording or see the errors and events captured during it, open an agent run's transcript or the written explanation on its automated quality scores, read a customer's profile properties, or search customers by email. Where a name or email would have labelled a row, a viewer sees the raw identifier instead. A viewer can still run AI investigations and read their own; an investigation someone else ran with access to recorded content shows its question and not its answer.

If someone in the viewer role needs any of that, change their role to member — the scopes are not grantable separately. A key can only receive scopes its creator holds, so a key created by a viewer can never carry one of the three.

Response and collaboration

Resource Scopes
Alerts alerts:read, alerts:acknowledge, alerts:resolve, alerts:rules:read, alerts:rules:write, alerts:rules:delete
Silences silences:read, silences:write, silences:delete
Delivery channels channels:read, channels:write, channels:delete
On-call oncall:read, oncall:contacts:write, oncall:write, oncall:delete
Escalation escalation:read, escalation:write, escalation:delete
Incidents incidents:read, incidents:respond, incidents:write, incidents:delete
Dashboards dashboard:read, dashboard:write, dashboard:delete
Connections and tickets connections:read, connections:write, connections:delete, tickets:read, tickets:write, tickets:delete
Account segments account_segments:read, account_segments:write, account_segments:delete

Responder actions such as acknowledging an alert or appending to an incident are separate from configuration and deletion. Give on-call automation the responder scopes it needs, not blanket administration.

Treat channels:read as a credential-grade scope. Reading a delivery channel returns its stored configuration as you saved it, and a webhook channel's configuration includes its URL and any request headers you attached — which is where an authorization token usually lives. A key that only needs to send telemetry should not carry it. Sending a test notification is channels:write, not channels:read, because it delivers a real message to that channel's recipients; a key used for test-sends needs both, since the test first reads the channel it is about to use.

Personal contact methods are self-service and are gated twice. oncall:read lists them and oncall:contacts:write adds, edits, or removes them; separately, every request is confined to the calling user's own contact methods, so no key can read or change someone else's. Both scopes are available to every role, including viewers — being reachable when you are on a rotation does not depend on your role. Note that an API key acts as the member who created it, so a key you did not intend to use for this still needs oncall:contacts:write withheld to be unable to change that member's paging destinations. Schedule administration stays on oncall:write and oncall:delete.

For dashboards, dashboard:write permits creation, cloning, and widget or layout changes. Changing dashboard metadata or variables is limited to the dashboard owner and organization admins, and deletion requires both dashboard:delete and dashboard-owner or organization-admin status. dashboard:read never makes a shared URL public; dashboard links still require an authenticated same-organization principal.

Organization, AI, and agents

Resource Scopes
Settings settings:read, settings:write, settings:delete
GitHub configuration github_config:read, github_config:write
Projects and members projects:read, projects:write, projects:delete, members:read, members:write
API keys api_key:read, api_key:write
Audit trail audit:read (owner/admin only)
Scores and evaluations scores:read, scores:write, evals:read, evals:write
Evaluation explanations agents:content:read (additionally reveals the explanation field on an evaluation result)
Customer Detective investigations:run (plus the scope for each kind of evidence an investigation should gather — see "An AI investigation reads with your key, not with ours")
Usage usage:read
MCP mcp:read, mcp:write
Documentation docs:read
Agent containment quarantine:request, quarantine:approve (owner/admin only)
Content governance governance:read, governance:write (write is owner/admin only)

Settings permissions are deliberately separate. settings:read shows stored organization and global LLM pricing overrides without edit controls; it does not expose the embedded fallback catalog. Adding or editing an organization override requires settings:write, and deleting one requires settings:delete.

The same three scopes cover the organization record and its configuration entries. Reading the organization or its configuration requires settings:read; renaming the organization or writing a configuration entry requires settings:write; deleting the organization or removing a configuration entry requires settings:delete. A key holding none of them authenticates normally and receives 403 PermissionDenied on all of them — which is the point: an unscoped key must not be able to delete your organization. Changing a plan or a usage limit is not available to any organization role, including owner.

Listing projects and listing the member roster are reads in their own right: projects:read returns each project's name, slug, id, and retention — whether you list them all or fetch one by id — and some endpoints need it indirectly, because exporting data validates the project you name before it accepts the job. members:read returns the roster including pending invitations, which are the not-yet-accepted half of the same list. Both are available to every role, because the project selector and the member picker appear throughout the product; creating, renaming, or deleting a project, and administering members or invitations, remain owner/admin-only under projects:write, projects:delete, and members:write. A key without the read scopes authenticates normally and receives 403 PermissionDenied on those endpoints, so an ingestion-only credential cannot enumerate your projects or your people.

Key management is itself scoped. Listing the organization's keys requires api_key:read, and creating, renaming, revoking, or re-scoping one requires api_key:write. A key without them authenticates normally but receives 403 PermissionDenied on those endpoints — listing exposes every key's id, name, and granted scopes, so it is not something an unscoped credential should reach. Granting api_key:write does not widen a key: a new key can still only receive scopes its creator already holds. Nor is it a way around a containment: a key frozen by an approved quarantine refuses a scope change until the quarantine is lifted, and lifting takes the owner/admin quarantine:approve. Leave both off any key that does not administer credentials.

audit:read opens the organization audit trail in Settings and through the account audit endpoint. It can be filtered by resource, action, and result; it omits network identifiers, user agents, and arbitrary metadata. Owners and admins receive it through their role, while narrower keys must name it explicitly.

mcp:write is an umbrella gate. An MCP write also requires its domain scope. For example, creating an incident requires both MCP write access and the incident permission enforced by that tool. Exact telemetry deletion requires mcp:write plus traces:delete or logs:delete on a project-scoped key; the MCP actions do not accept a project override.

Containing a misbehaving agent takes two people. quarantine:request lets any member or developer ask for an asset, an observation key, or a single API key to be frozen, revoked, or both — asking enforces nothing on its own. quarantine:approve is owner/admin-only, and it is what actually disables the credential: approving is what signs and enforces the request, and lifting a containment afterward takes the same scope. No key can hold only quarantine:approve — every role that grants it also grants quarantine:request — but the platform separately refuses to let one principal both request and approve the same containment, so a key that could do both still cannot approve its own ask.

Withdrawing a request nobody approved is the exception, and it sits with quarantine:request: retracting your own ask hands capability back to nobody, because a pending request never touched a credential. Retracting somebody else's takes quarantine:approve — it suppresses a containment before a second person can consider it. See Revoke an agent's access for the full request-approve-lift flow and Withdraw a request nobody approved.

Your project's content policy

Your project's content policy says which kinds of recorded content may cross which boundary: model transcripts, tool arguments and results, session-replay recordings, the property bag on a person, the separate property bag on a business account or group, and agent memory — each against being stored at all, served to a reader, included in a bulk export, or sent to an evaluation model. governance:read returns that grid for one project or for every project in the organization. It is available to every role, including viewers, because the grid contains no recorded content of any kind — only the decisions and the version — and the people a disclosure control protects should be able to check it.

governance:write replaces a project's policy and is owner/admin-only. It is the broadest single change the API offers: widening one cell makes every transcript in that project readable, exportable or shippable to an evaluation model, retroactively, for every key that already holds the matching content scope. Leave it off any key that does not administer disclosure policy.

Two things are worth knowing before you rely on it. Every project already has a policy, including one you have never opened: the organization seeds each project with a grid matching what the product does today. A project whose grid has not been written yet reads back as fully denied rather than as an error — that is the shape of the response, not what the boundaries do in the meantime, which is covered below. And a change takes up to 30 seconds to take effect everywhere, so a tightening is not instantaneous.

The four boundaries are not equally reversible, and one of them is permanent. Refusing a class at the reading, exporting or evaluation boundary withholds a copy of something Anectico still holds, so widening the policy and asking again returns it. Refusing a class at the storing boundary means the content is never written down: the observation is kept — timing, status, model, token counts, cost, and the person it belongs to — and the bodies are not, for good. Widening applies forwards only and cannot recover what was recorded while the class was refused. The 30-second propagation therefore cuts both ways at that boundary: a tightening leaves up to 30 seconds of content recorded (which you can delete), and a widening leaves up to 30 seconds of content unrecorded (which you cannot recover).

At the reading boundary, "we do not know yet" means withhold — the opposite of storing. If Anectico briefly cannot read your project's policy, reads return the observation with the content left out rather than guessing that you would have allowed it. Nothing is lost: the content is still stored, and the same request returns it once the policy can be read again. The reason the two boundaries answer the same uncertainty in opposite directions is the reversibility above — a read you have to repeat costs a retry, and content disclosed against a refusal you had just recorded cannot be taken back. The evaluation boundary answers this uncertainty the same way reading does — if the policy cannot be read, nothing is sent to a model — because its destination is outside Anectico and outside your organization, which is the one place a mistaken send is worst.

A project whose policy has not been set up yet is treated as if it already had the standard one, at all four boundaries. A brand-new project gets its policy from a sweep that runs every fifteen minutes rather than at the moment you create it, so there is a short window before its grid exists. In that window the project behaves exactly as it will once the grid is written — which is the point: the alternative answers the same project two different ways depending on which side of the sweep a request lands, and records a refusal against a preference you had not expressed. Nothing is decided differently because of it; the standard posture is what your project is about to be given, and you can tighten it the moment it exists.

Exporting was the last boundary to work this way, and the change is visible on the export itself: a job decided this way is marked as having used the standard posture rather than a grid you set, so a file assembled before your project's grid existed is never mistaken for one you had approved.

An export that covers a whole organization rather than one project is the exception, and still leaves recorded content out. There is no single grid to apply — the projects it spans may each have set a different one — so nothing is assumed on their behalf. Scope the export to a project to get its content.

Reading, exporting, and automatic evaluation are independent boundaries. A class set to deny at read and allow at export can still be exported, and the same combination with judge_transfer can still be used by an automatic quality check. Each operation asks the cell for the boundary it is actually crossing. Customer Detective is intentionally different: it assembles a user-visible investigation first, so its evidence must pass read as well as the later judge_transfer decision. Denying any one cell affects only operations that cross that boundary.

A read that covers your whole organization is governed row by row. A content policy belongs to a project, and a request that names no project can return rows from projects whose policies disagree. Each row is answered by the policy of the project it came from — so one organization-wide search of traces or logs returns content from the projects that permit it and leaves it out for the projects that refuse, in the same response, and says which rows were affected. This is what you get from the API, the MCP endpoint and the CLI when you do not pass project_id.

Two things still come back without content, and they are fixed in different places. Trace exemplars on a metric point carry no project of their own, so a metric read that names no project cannot obey a policy and leaves their attributes out — name the project (project_id) and the content comes back. And a row that carries no project at all, because the telemetry was sent with an organization-level credential rather than a project one, has no policy that could govern it whichever way you read it; sending that telemetry with a project-scoped credential is what fixes that one, not the read. The dashboard always names a project for you.

At the storing boundary, "we do not know yet" means record. A project whose policy has not been set up yet, and a project whose policy Anectico briefly cannot read, both keep recording on the standard posture rather than being refused. A refusal you have already configured is not affected — the last policy read for your project stays in force through any outage of ours. The reasoning is the asymmetry above: content recorded in error can be deleted afterwards, and content never recorded cannot be brought back.

Session-replay recordings and person property bags are now enforced at the storing and reading boundaries too. Every class in the grid except agent memory now binds at every boundary where it has a flow, and the two that recently did not behave like this:

  • Session replay at store. A recording chunk is refused outright — no recording appears, and the upload is answered with an error naming the content policy rather than being accepted and thrown away. What a session was linked to is unaffected: the errors and product events captured during a session, and a recording's existence, length and person, are not the recording itself and are never withheld under this rule.
  • Session replay at read. Playback is refused with an error rather than an empty timeline, because an empty timeline is indistinguishable from a session that recorded nothing. The decision follows the recording's own project, so an organization-wide credential gets the right answer without naming one.
  • Person properties at store. The property values on an identify() call are not written down; the identity itself still is. Anectico still learns who the person is, still merges an anonymous visitor into an identified one, and still links their sessions, errors and traces — only the property values are dropped, and the response says so. Clearing a property (unset) is never refused: it removes recorded content rather than recording any.
  • Person properties at read. The property bag is left off a person, per person, according to the policy of the project that person belongs to — so one organization-wide list can return properties for the projects that permit them and leave them out for the projects that do not, in the same response. The record says which control removed them, because only one of the two is something you can fix with a different key.

The storing boundary also reaches the context you attach to an error. Tags, extra data and captured request headers on an error report are your own key/value bags, so a field you name after a model-content convention — extra.gen_ai.prompt.0.content, say — holds the same bytes the policy governs on a span, and is treated the same way. A refusal drops those fields from the error and from its issue, and names them in anectico.content.not_stored; every other field you sent is untouched.

Event properties are governed at BOTH boundaries. The property bag on a product event is your own key/value bag, so a property named after a model-content convention is the same bytes the policy governs on a span. At the storing boundary those properties are not written down. At the reading boundary — a customer's activity timeline, or an account's — they are left off the event and the response names them, per event, according to the policy of the project that event belongs to. So one organization-wide timeline can return properties for the projects that permit them and leave them out for the projects that do not, in the same response. Every other property you sent is untouched, and so is the event itself: its name, its time, the person and the session it belongs to.

A promoted evaluation case is refused rather than trimmed. When you promote a production agent run into a dataset, the run's own recorded content is governed as usual — refused classes are left out of the frozen case and the case records what went. The expected and metadata documents you send with it are treated differently: if one of them carries a field named after a model-content convention that your policy refuses to store, the whole promotion is refused and nothing is written. The response names the document and the field so you can remove it and retry.

The reason is that a dataset version cannot be edited. Once written, its cases are permanent — that is what makes an evaluation result citing a version mean something. Quietly dropping a field from an expected answer would leave a permanent, unremovable specification you did not write, scoring differently for ever, and nothing in the record would say so. Refusing costs you one edit; you are still holding the document.

Reading a dataset is governed too, and separately: a case's frozen run keeps its shape with the recorded bodies removed, while expected and metadata are withheld whole and replaced by a note saying which field was refused and whether it was your policy or your key that refused it. That matters because the two boundaries are separate settings — a project may record content and refuse to hand it back — and because a policy you tighten today applies to cases promoted before you tightened it.

Two consequences are worth knowing before you set either to deny:

  • People search stops matching on property values for a project that refuses them at read. It has to: leaving the search able to match and rank on a value it will not print would answer the question the withholding exists to refuse. Searching by any identifier you sent with identify() keeps working. A people search that names no project takes the same narrower behavior whatever your policy says, because the search has to be built before it knows which projects the results will come from — name a project to keep property matching.
  • Trace and log search stops matching on withheld attribute values, for the same reason. Free-text search keeps working over the parts it may still show you — the message, service and operation names, and identifiers — so most searches are unaffected. Searching for an exact value of a withheld attribute is refused with 400 rather than quietly answered from a narrower set, because a filtered result you cannot tell apart from an unfiltered one is worse than an error. This applies to trace search, log search, live tail, unified search and correlation. As with people search, a request that names no project takes the narrower behavior whatever your policy says — name a project to keep attribute matching.
  • Property-based audiences stop refreshing for a project that refuses person properties at read, because an audience defined by a property value is computed by reading those values. Audiences defined by behavior are unaffected.

Agent memory has no producer at all yet, so its four cells govern nothing whichever way you set them; that is stated rather than hidden, because a control you believe in and do not have is worse than one we do not offer. Note that documents retrieved by a RAG or search step are recorded as part of the model transcript and are governed by that class, not by agent memory. Session replays and person profiles are still also governed by their own permissions (replay:content:read, persons:profile:read); the two controls compose, and neither stands in for the other.

Records and responses affected this way say so, and the three statements are deliberately different:

Field on the record What it means What changes it
anectico.content.not_stored The content was never written down, because your policy refused that class at the storing boundary Nothing — this is permanent. Widening the policy applies forwards only
anectico.content.policy_withheld The content exists and your project's policy refuses that class at this boundary A change to the content policy
anectico.content.withheld The content exists, your policy permits it, and this credential is not in the sensitive-content tier A key with the matching content scope

Where both a policy refusal and a credential gap apply to the same field, the policy is what is reported, because a wider key would not have helped. All three fields are written by Anectico and only by Anectico: they are stripped from anything you send — span and log attributes, product-event properties, and error tags, extra data and captured headers alike — and re-issued from what was actually removed, so they cannot be forged into your own telemetry. anectico.content.* is a reserved key prefix for that reason; a key of your own with one of those three names is dropped rather than stored, whatever your policy permits. See Prompt and completion content.

Reading the documentation from an agent

docs:read lets a key read Anectico's own product documentation — these pages, byte for byte — through the search_docs and get_doc actions on the MCP endpoint, and through the docs source on search and the doc reference kind on fetch. It is the one scope on the surface that discloses nothing about your organization: the pages it returns are published at anectico.com/docs and are the same for everyone.

It is available to every role, including viewers, for that reason. It exists as a scope so a key can deliberately leave it out — a narrow ingest or cleanup key has no use for the manual — not because the content needs protecting. Give it to any agent you expect to configure something: an agent that can look up a scope name, an alert expression or an SDK call is an agent that stops guessing them.

Safe key patterns

  • Browser or mobile SDK: project-scoped ingest:write plus analytics:write; add replay:write only when recording browser replay. Browser flag evaluation additionally needs flags:read; treat all flag configuration and payloads as client-visible.
  • Server SDK: project-scoped ingest:write, adding analytics:write when it calls identify, group, screen, or diagnostic-event methods.
  • OpenTelemetry collector: project-scoped ingest:write only.
  • Any agent you expect to configure something: add docs:read so it can look up a scope name, an alert expression or an SDK call instead of guessing one. It reads published documentation and nothing about your organization.
  • Read-only agent: mcp:read and only the read domains it needs. services:read, agents:read, and llm:read are each sufficient on their own for service health, agent-run summaries, and spend — a read-only agent does not need traces:read for them. Add persons:profile:read, replay:content:read, or agents:content:read only when the agent must read customer content verbatim; withholding them is enough to withhold that content, including the prompts and completions third-party AI instrumentation records as span attributes. traces:read on its own now returns those spans with the bodies removed and an anectico.content.withheld attribute naming what was removed, so an agent can investigate latency and errors on traces without ever reaching a transcript. Your project's content policy can withhold the same bodies from a key that does hold the content scope; those responses carry anectico.content.policy_withheld instead.
  • Telemetry-cleanup agent: dedicated project key with mcp:write and only traces:delete and/or logs:delete; both actions preview the signed project and exact target before confirmation.
  • CI symbol upload: dedicated project key with errors:write; add releases:write when the same job also registers a release.
  • Release pipeline registering agent inventory: dedicated project key with agents:write, minted by an owner or admin. It covers the deployment, the effective configuration and the bill of materials — all three legs of one declaration. Add agents:read only if the same job reads the registration back or checks the review queue it feeds; the write does not admit any read on its own.
  • Operations automation: explicit responder or configuration scopes, fixed project, no unrelated deletes.

The wildcard platform-admin permission is not grantable to API keys.

persons:correct permits an audited alias separation with a current preview revision. Granted to owners/admins only; API keys may request it explicitly. persons:read permits preview and mapping history without profile property values. SDK analytics:write does not permit corrections.