agent-permission-scoping
Skill0 starsChanged 6 months agoMITView on GitHub
Scope tool execution behind permission levels (read_only, mutate, admin). Use when different users or agent roles should have different tool access rights, injecting the permission level via ToolContext metadata and declaring requirements via @set_metadata.
In this repository65 other files it gives its agents.
- AGENTS.mdAGENTS.md
- skills/building-agents/agents.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- llms-full.txtllms.txt
- llms.txtllms.txt
- adding-guardrailsSkill
- agent-health-timeoutSkill
- agent-session-isolationSkill
- agent-step-loggingSkill
- agent-supervisor-routerSkill
- agent-telemetrySkill
- async-agent-backgroundSkill
- building-agentsSkill
- building-teamsSkill
- building-toolsSkill
- calendar-scheduling-toolSkill
- callback-tracing-handlerSkill
- chain-of-thought-promptingSkill
- chat-model-configurationSkill
- code-execution-sandboxSkill
- common-agent-patternsSkill
- context-window-managementSkill
- conversation-memorySkill
- conversation-state-dbSkill
- database-query-toolSkill
- document-loadersSkill
- document-rerankingSkill
- email-notification-toolSkill
- embedding-model-ingestionSkill
- fallback-model-chainSkill
- few-shot-injectionSkill
- filesystem-toolSkill
- human-in-the-loopSkill
- hybrid-searchSkill
- inspecting-streamsSkill
- integrating-with-laurenSkill
- json-schema-enforcementSkill
- llm-provider-connectionSkill
- llm-rate-limitingSkill
- long-term-vector-memorySkill
- managing-memorySkill
- migrating-to-lauren-aiSkill
- multi-agent-delegationSkill
- multi-tool-orchestrationSkill
- multi-turn-tool-stateSkill
- output-guardrailsSkill
- plan-and-execute-agentSkill
- prompt-injection-defenseSkill
- prompt-versioningSkill
- rag-pipelineSkill
- react-agent-loopSkill
- response-validation-retrySkill
- rest-api-toolSkill
- securing-agentsSkill
- semantic-cacheSkill
- streaming-response-handlerSkill
- structured-output-parsingSkill
- sub-agent-spawningSkill
- system-prompt-templatingSkill
- testing-agentsSkill
- The first 60 shown.
The file
Files
Use
codemap find "ToolContext"to check the metadata injection API.
Agent Permission Scoping
Define an ordered Permission enum and check permissions inline in the tool's
run() method. Declare the minimum permission level with @set_metadata for
introspection and documentation. Inject the caller's permission via
runner.run(metadata={"caller_permission": ...}).
Permission model
from enum import Enum
class Permission(str, Enum):
READ_ONLY = "read_only"
MUTATE = "mutate"
ADMIN = "admin"
PERMISSION_ORDER = {
Permission.READ_ONLY: 0,
Permission.MUTATE: 1,
Permission.ADMIN: 2,
}
def _check_permission(ctx: ToolContext, required: Permission) -> dict | None:
"""Return error dict if caller lacks required permission, else None."""
granted_str = ctx.get_metadata("caller_permission") or Permission.READ_ONLY.value
try:
granted = Permission(granted_str)
except ValueError:
granted = Permission.READ_ONLY
if PERMISSION_ORDER[granted] < PERMISSION_ORDER[required]:
return {"error": f"Permission denied: requires {required.value}, got {granted.value}"}
return NoneProtected tool
Apply @set_metadata first (outermost), then @tool(). The @set_metadata
decorator stores the declared minimum permission on the tool class for
documentation and introspection. Permission enforcement happens in run().
from lauren_ai._tools import ToolContext, set_metadata, tool
@set_metadata("min_permission", "mutate")
@tool()
class UpdateRecordTool:
"""Update a database record.
Args:
record_id: The record to update.
data: The new data.
"""
async def run(self, ctx: ToolContext, record_id: str, data: str) -> dict:
err = _check_permission(ctx, Permission.MUTATE)
if err:
return err
return {"updated": record_id, "data": data}Injecting permission at call time
Pass the permission level as metadata when invoking the runner or TestClient:
# Production runner
response = await runner.run(
agent_instance,
"Update record 42 with new data",
metadata={"caller_permission": "mutate"},
)
# In tests via TestClient
result = client.run("Update record 42", metadata={"caller_permission": "mutate"})ToolContext.get_metadata("caller_permission") checks tool-level static
metadata first, then delegates to AgentContext.get_metadata, which reads from
the metadata dict supplied to runner.run.
AgentRunner test pattern
import json
from lauren_ai._agents import AgentContext, agent, use_tools
from lauren_ai._tools import ToolContext, ToolResult, set_metadata, tool
from lauren_ai._transport import Completion, TokenUsage
from lauren_ai.testing import TestClient
class _Capture:
def __init__(self):
self.captured: list[ToolResult] = []
async def on_tool_result(self, result: ToolResult, ctx: AgentContext) -> ToolResult | None:
self.captured.append(result)
return None
@agent(model=None, system="Permission test agent")
@use_tools(UpdateRecordTool, DeleteRecordTool, ReadRecordTool)
class PermissionTestAgent(_Capture):
def __init__(self):
_Capture.__init__(self)
def _c(text):
return Completion(id="c1", model="mock", content=text, tool_calls=[],
stop_reason="end_turn", usage=TokenUsage(10, 5))
def test_mutate_permission_allows_update():
agent_inst = PermissionTestAgent()
client = TestClient(agent_inst)
client.mock.queue_tool_use("update_record_tool", {"record_id": "1", "data": "x"})
client.mock.queue_response(_c("Updated."))
client.run("Update record", metadata={"caller_permission": "mutate"})
out = json.loads(agent_inst.captured[0].content)
assert "error" not in out
assert out["updated"] == "1"
def test_read_only_permission_blocks_update():
agent_inst = PermissionTestAgent()
client = TestClient(agent_inst)
client.mock.queue_tool_use("update_record_tool", {"record_id": "1", "data": "x"})
client.mock.queue_response(_c("Denied."))
client.run("Update record", metadata={"caller_permission": "read_only"})
out = json.loads(agent_inst.captured[0].content)
assert "error" in outReading declared minimum permission in the tool
required = ctx.get_metadata("min_permission") # → "mutate" (from @set_metadata)
granted = ctx.get_metadata("caller_permission") # → from runner.run(metadata=...)ctx.get_metadata checks tool-level static metadata (TOOL_METADATA attribute)
first, then agent-level runtime metadata (AgentContext.metadata).
Notes
- Permission strings are passed as plain values in
metadata. @set_metadatais read-only at call time — it's for declaration and introspection, not enforcement. Always enforce inrun().- Decorator ordering matters:
@set_metadatamust be outermost (applied last),@tool()must be innermost (applied first) so that@tool()inspects the originalrun()signature withctx: ToolContext. - For request-level permission injection in a Lauren web app, extract the
permission from
request.statein the controller and forward it asmetadata={"caller_permission": user.role}.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

