Debug PERMISSION_DENIED errors¶
A call returned PERMISSION_DENIED (or Forbidden, or
MEMORY_ACCESS_DENIED). What's actually wrong?
Hadron's memory access is governed by a four-gate cross-org chain plus a strict-ownership rule for personal memory. The error tells you a gate closed; it doesn't tell you which one. This guide walks the four most common cases, each with a diagnostic query and a concrete fix.
For the conceptual model — what each gate is and why it exists — see Memory access. Read that first if the gate names below feel abstract.
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 (Settings → Members 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.