Debug PERMISSION_DENIED errors¶
Someone can't see a memory they should, or an App's call is refused, and all
the error says is PERMISSION_DENIED, Forbidden or MEMORY_ACCESS_DENIED.
Find out whether they actually have access, which grant gives it, or that none
does. Then find the gate that is blocking them and open it. The first check is a
form in the portal. The deeper diagnostics further down are GraphQL queries
for an administrator. Most fixes are a portal setting, or a request to the
admin who owns the grant.
The error tells you a gate closed; it doesn't tell you which one. Hadron's memory access runs through a four-gate cross-org chain, plus a strict-ownership rule for personal memory. For what each gate is and why it exists, see Memory access. Read that first if the gate names below feel abstract.
First, check their access in the portal¶
Ask the server directly: does this person have access to this resource, and through which grant?
- In the portal, press ⌘K (Ctrl K on Windows and Linux) and type
check access. Choose Check access. - In User, enter the person:
@handle,hrn:user:<handle>, or a user id. It starts filled in with you. - In Resource, enter the full URN of the memory, node, agent, App,
organization or user, for example
hrn:mem:acme.com:market, or the id of an AI config. - Choose Check access.
What you get back: their Role, a tick or a cross for each of READ, WRITE, MANAGE and DELETE, and a Grants table. Each row of the table shows a Source, a Role and a Via, naming the grant that confers the access. The answer comes from the same resolver that enforces access, so it is authoritative for what that person's grants allow on that resource. It doesn't see the context of a particular failing call, such as which App it came through or which credentials it used.
What it tells you:
- Grants listed. They do have access, and the table names where it comes from. If their call still fails, the problem is elsewhere: the wrong App context, a stale token, or a different user than you think. The gate-by-gate walk below explains the rest.
- "No grants — this user has no access to this resource." They really have no grant. Use the cases below to find the closed gate and open it.
Anyone can check their own access. To check someone else's, you need the right to audit the resource. That means being a platform admin, an ADMIN or OWNER of the organization that owns it, or, for a personal memory, its owner. Without that, the portal says "You're not allowed to audit access to this resource." That is you being blocked, not the person having no access.
An agent connected over MCP can't run this check, so ask in the portal rather than in chat.
The same check from the CLI¶
access check returns the server-computed effective capabilities
(read/write/manage/delete) plus the grants that confer them —
straight from the authoritative effectiveAccess resolver, so it
never disagrees with the enforced decision. See
Access checks for the
full contract.
Two outcomes tell you where to go next:
- A non-empty grants list — the user does have access, and the
SOURCE/VIAcolumns name the grant. If a call still fails, the problem is elsewhere (wrong App context, a stale token, a different user than you think); the gate-by-gate walk below explains the rest. - An empty grants list ("No access") — the user genuinely has no grant. Fall through to the cases below to find the closed gate and open it.
Two caveats:
- Checking someone else needs audit rights. Checking yourself is
always allowed. For another user,
access checkrequires a platform admin, an ADMIN/OWNER of the resource's owning org, or (for a strict-owner personal memory) the memory's principal. If you lack them, the command exits 1 (FORBIDDEN); that is the auditor being blocked, not the subject having no access. - It answers for one
(user, resource)pair. It does not list every user who can reach a memory — for that usememory member/memory share.
Triage in 30 seconds¶
| Symptom | Likely case | Jump to |
|---|---|---|
| App-keyed call fails; nothing user-related involved. | Cross-org install grant inactive. | Case 1 |
| Reading a knowledge memory fails; agent and memory are in different orgs. | Agent not attached to the memory, or cross-org subscription missing. | Case 2 |
| Mutation in the portal — toggling a setting, attaching a memory — denies for a CONTRIBUTOR / READER. | Admin-gated mutation called by non-admin. | Case 3 |
| Personal-memory call denies even though the user is signed in. | AgentSubscription revoked, expired, or wrong-App. |
Case 4 |
If none of the above fits, jump to Other gates.
Case 1: Cross-org install grant inactive¶
Gate: 1 (App ↔ Agent).
Symptom: an App-keyed call from one org to an Agent owned by another org is denied. The same Agent works fine when called from its owning org.
Diagnose¶
Check whether the calling org has an active AgentOrgGrant for the
Agent:
query CheckGrants($orgId: ID!) {
organization(id: $orgId) {
name
agentOrgGrants {
agent { id urn name }
isActive
activatedAt
revokedAt
}
}
}
If the Agent isn't in the list, no grant row exists. If isActive
is false or revokedAt is set, the grant is closed.
Fix¶
Cross-org grants auto-provision when the calling org first calls
createApp (with the Agent referenced) or publishAgentImport.
Revoking happens explicitly. To restore access:
- No grant row at all: have the calling org's admin run the install flow (Building an agent, Step 3). The grant gets created on first install.
- Grant exists but
isActive: false: the Agent's owning-org admin revoked it. Reach out to them to re-activate. - Same-org call denied at this gate: this shouldn't happen —
same-org installs auto-pass Gate 1. If you see it, the App's
agent_idis null. Inspect the App and re-create it pointing at the right Agent.
Case 2: Agent not attached to the memory¶
Gate: 2 (Agent ↔ Memory).
Symptom: the Agent's chat or tool-use returns "memory not found" or denies on read of a knowledge memory the Agent is supposed to use.
Diagnose¶
List the agent's attached memories from the agent's owning org:
query AgentMemories($orgId: ID!) {
organization(id: $orgId) {
agents {
id name urn
memoryItems {
memory { id urn name visibility }
role
}
}
}
}
If the memory you expect isn't in the agent's memoryItems, the
attachment doesn't exist. If it's there but role: read and you
were trying to write, see Case 3
and the Gate 4 effective-role rule.
For a cross-org knowledge memory (the Agent in Org B reads a
memory published by Org D), also confirm the MemorySubscription:
If the cross-org memory doesn't show up in myMemories for an org
admin who should see it, the subscription is missing or revoked.
Fix¶
- No
AgentMemoryItem: an admin of the agent's org attaches the memory through the agent detail page (Memory tab → Add memory). - Cross-org
MemorySubscriptionmissing: the publishing org (Org D) controls who can subscribe. Admins of Org B request a subscription through whatever onboarding the publishing org provides; today this is a manual mutation rather than a self-service UI.
Case 3: Admin-gated mutation called by non-admin¶
Gate: 4 (effective role).
Symptom: a portal toggle (asset upload, AI config save,
visibility change, member promotion) saves with "permission
denied" or Forbidden for a member who can read the page.
A subset of mutations require org ADMIN or OWNER of the agent's organization, not just ORG membership. The portal usually hides the controls from non-admins, but a few render the controls and fail on save — and any caller hitting these mutations directly through GraphQL or the SDK will see the same error.
Note: pre-008 there was an AppAgent.role cap that gated some of
these. Post-008 it's gone — system memory is universally read-only
from any App, and admin gating is enforced at the resolver layer.
Diagnose¶
Check your role on the relevant org:
Find your user in members and inspect role. If it's
CONTRIBUTOR or READER, you cannot run admin-gated mutations.
Fix¶
- An existing
OWNERorADMINof the org promotes you (the organization's Team tab in the portal). - Or: the action is taken by an existing admin on your behalf.
Mutations known to be admin-gated include
setAgentAssetUploadEnabled (toggling the asset-upload tool), AI
config writes for agent and app surfaces, and most cross-org
configuration changes. The error message itself doesn't always
say "admin only" — treat any Forbidden on a configuration write
as a candidate for this case.
Case 4: Personal memory blocked¶
Gate: 3 (User ↔ Agent) plus the strict-ownership rule.
Symptom: a chat that worked yesterday now denies. The user is signed in. The Agent is correctly installed in the calling App's org. Nothing about the Agent's setup changed.
There are three sub-shapes here. Diagnose first, then fix.
4a — Subscription revoked, expired, or never activated¶
query MySubs {
myAgentSubscriptions {
agent { id urn name }
isActive
activatedAt
expiresAt
revokedAt
}
}
If the agent isn't in the list, no subscription was provisioned —
this is normal for userId = "anonymous" callers (no auto-provision
on anonymous identity). For an authenticated user, an agent should
appear after their first chat.
If isActive: false:
revokedAtis set → an admin or the user themselves revoked it. Re-activate by sending a fresh chat (the auto-provision rebinds if revocation isn't sticky in your config) or have an admin of the agent's org re-activate the row.expiresAtis in the past → the subscription has a finite lifetime that lapsed. Re-subscribe.
4b — Cross-App isolation (different App, same Agent)¶
This is the subtle one. A user's personal memory is keyed on
(app_id, user_id), not on agent. If the same Agent is
installed in two Apps (say, Juno-on-MicroMentor and
Juno-on-CompanyX), each install has its own personal memory. A
chat through CompanyX's App cannot read what was written through
MicroMentor's App.
Confirm by listing the Apps in the calling org and checking which one is making the call:
If you see two Apps both pointing at the same Agent, the personal memories are isolated by design. This is not a bug — it's the multi-install isolation property Memory access exists to enforce.
4c — User trying to read someone else's personal memory¶
The strict-ownership rule says only Memory.userId can read
their own personal memory. Not org admins. Not platform owners.
Not the Agent's developer. Not anyone.
If you're hitting this trying to support a user, you can't read the data directly. Help them through the issue from their side (authenticated as them), or guide them to extract whatever they need themselves.
Fix¶
- 4a: re-subscribe (or have an admin re-activate the
AgentSubscription). - 4b: not a fix — accept the isolation. If the user actually needs cross-App memory, the architecture has to change (file this as a feature request).
- 4c: redirect through the user. Hadron's strict ownership is non-negotiable.
Other gates¶
The four cases above cover the high-frequency failure modes. The
Memory access
explanation page has a more exhaustive symptom-to-gate table for
edge cases — visibility mismatches, role caps via
MemorySubscription, and the "App-keyed call without an App in
context" debug shape.
If the diagnostic queries above all return clean results and the call still fails, capture the exact error message and the GraphQL operation that triggered it, then file an issue at hadron-memory/hadron-server.
Related¶
- Memory access — conceptual model of the four gates and the strict-ownership rule.
- Subscriptions —
AgentOrgGrantandAgentSubscriptionin detail. - Building an agent — the install flow that auto-provisions cross-org grants.
- Configure your LLM provider — one of the admin-gated mutations from Case 3.