zeroclaw-labs/zeroclaw · error
AgentScopedMemory refuses purge_namespace: cross-agent bulk
Error message
AgentScopedMemory refuses purge_namespace: cross-agent bulk delete must run through an admin Memory handle
What it means
AgentScopedMemory is a security wrapper around a Memory backend that is bound to exactly one agent_id (plus a recall-only allowlist of peers). The Memory trait's purge_namespace would bulk-delete rows of ALL agents in a namespace, so the wrapper refuses the call unconditionally — there is no input that makes it succeed — and logs a WARN Reject event with the namespace and bound_agent before bailing. The intended operator path for a namespace purge is an admin Memory handle on the same backend (e.g. the inner SqliteMemory/PostgresMemory), not the agent-scoped wrapper handed to an agent loop.
Source
Thrown at crates/zeroclaw-memory/src/agent_scoped.rs:375
})
.count())
}
async fn purge_namespace(&self, namespace: &str) -> Result<usize> {
// Bulk cross-agent destruction has no agent-scoped form on the
// trait. Refuse rather than passing through; the operator path
// for purges is an admin Memory handle, not an agent loop.
::zeroclaw_log::record!(
WARN,
::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
.with_outcome(::zeroclaw_log::EventOutcome::Failure)
.with_attrs(::serde_json::json!({
"namespace": namespace,
"bound_agent": self.agent_id,
})),
"purge_namespace refused: cross-agent bulk delete requires an admin Memory handle"
);
anyhow::bail!(
"AgentScopedMemory refuses purge_namespace: cross-agent bulk delete must run through an admin Memory handle"
);
}
async fn purge_session(&self, session_id: &str) -> Result<usize> {
// Bulk session deletes must be scoped by both session and bound
// agent at the backend boundary. Listing a session and deleting by
// `(key, agent_id)` can delete the bound agent's row from a
// different session when keys collide.
self.inner
.purge_session_for_agent(session_id, &self.agent_id)
.await
}
async fn reindex(&self) -> Result<usize> {
// Reindex is an admin-shaped op (rebuilds FTS / re-embeds
// missing vectors). Touching the inner backend here is
// contained: it does not mutate row attribution or exposeView on GitHub (pinned to 88bb9c8533)
Solutions
- Obtain an admin Memory handle to the same backend (construct/open the inner backend directly instead of going through AgentScopedMemory) and run purge_namespace there, in operator-controlled code.
- If you only meant to clear one session's data, call purge_session(session_id) on the scoped handle — it deletes only the bound agent's rows via purge_session_for_agent.
- If you meant to delete specific keys, use forget/forget_for_agent, which the wrapper permits for the bound agent.
- If you expected to already hold an admin handle, audit how the handle was created/wired — an AgentScopedMemory was injected somewhere in the chain.
Example fix
// before: agent-scoped handle, always refused
let removed = scoped.purge_namespace("default").await?; // bails: refuses purge_namespace
// after: run the bulk delete through an admin handle on the same backend
let admin: Arc<dyn Memory> = Arc::new(SqliteMemory::new(alias, workspace_dir)?);
let removed = admin.purge_namespace("default").await?; Defensive patterns
Strategy: fallback
Try / catch
// Route the refusal instead of crashing the agent loop
match scoped.purge_namespace(ns).await {
Ok(n) => log::info!("purged {n} rows"),
Err(e) if e.to_string().contains("AgentScopedMemory refuses purge_namespace") => {
// Escalate to the operator/admin-handle path; never retry on the scoped handle
return admin_queue.enqueue_purge(ns);
}
Err(e) => return Err(e),
} Prevention
- Never call purge_namespace on a handle you did not construct yourself; treat trait objects from DI as possibly agent-scoped.
- Keep admin operations (purge_namespace, reindex) in operator-run code paths that open the backend directly.
- Prefer purge_session or forget on scoped handles — they are the agent-safe delete surfaces.
- Assert in tests which handle type your cleanup code receives before relying on trait parity.
When it happens
Trigger: Calling purge_namespace on any Memory trait object that is actually an AgentScopedMemory (constructed via AgentScopedMemory::new(inner, agent_id, ...)). Commonly hit when DI hands your loop the agent-scoped handle and you attempt an install-wide cleanup: scoped.purge_namespace("default").await. Note purge_session on the same wrapper succeeds because it is routed to purge_session_for_agent and stays agent-bound.
Common situations: Writing a maintenance/cleanup job that receives the same Memory handle the agent uses; refactoring admin tooling to reuse the runtime's scoped handle; tests that assert the refusal (purge_namespace_is_refused); assuming the trait method is callable on every implementation.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
- security.otp.cache_valid_secs must be greater than or equal
- security.otp.gated_actions[{i}] contains invalid characters:
- memory consolidation write denied by policy: {e}
- memory fact write denied by policy: {e}
- Embedding API error {status}: {text}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/7f4b3287280547f8.
Report an issue: GitHub.