Skip to main content
Allow-always grants now persist across gateway restart and default to being scoped to the approving agent.

Quick Start

1

Enable durable scoped approvals

Durable, agent-scoped grants are on by default — just launch the gateway. The allow-list opens a SQLite store at ~/.praisonai/state/gateway/approvals.sqlite.
from praisonai.gateway.exec_approval import ExecApprovalManager

manager = ExecApprovalManager()  # durable=True by default
2

Grant Once, Auto-Approve Forever (per Agent)

Resolve a pending request with allow_always=True. The grant is scoped to the resolving agent by default — one agent’s approval never authorises another.
from praisonai.gateway.exec_approval import ExecApprovalManager, Resolution

manager = ExecApprovalManager()

request_id, future = await manager.register(
    tool_name="shell_exec",
    arguments={"cmd": "ls -la"},
    agent_name="shell-bot",
)

# From your bot / UI handler:
manager.resolve(request_id, Resolution(approved=True, allow_always=True))

# Next identical call from "shell-bot" auto-approves — even after a restart.
3

Scope even tighter to arguments

Add scope_to_args=True so only subsequent calls with the same argument shape auto-approve; a different argument re-prompts.
from praisonai.gateway.exec_approval import Resolution

manager.resolve(
    request_id,
    Resolution(approved=True, allow_always=True, scope_to_args=True),
)
4

Grant globally (legacy behaviour)

Set scope_to_agent=False for the pre-#2622 semantic where one approval covers every agent.
from praisonai.gateway.exec_approval import Resolution

manager.resolve(
    request_id,
    Resolution(approved=True, allow_always=True, scope_to_agent=False),
)

How It Works

A matching grant short-circuits register — the future resolves immediately with approved=True and no prompt is shown.

Which Scope to Use

Pick the narrowest scope that still saves the operator from re-approving.

The Grant Key

Every grant is stored as (agent_id, tool_name, arg_signature).
Key partTypeMeaning
agent_idstrResolved agent name; sentinel "*" means “any agent”
tool_namestrRegistered tool name
arg_signaturestr | NoneFirst 32 chars of SHA-256 over json.dumps(args, sort_keys=True); None = argument-agnostic

Resolver Options

Resolution carries the resolver’s decision and how broadly to persist it.
FieldTypeDefaultDescription
approvedbool(required)Whether the request is approved
reasonstr""Human-readable reason
allow_alwaysboolFalsePersist the grant; when False the resolution applies only to this one request
scope_to_agentboolTrueGrant is scoped to the requesting agent; set False for pre-#2622 global grants
scope_to_argsboolFalseAlso key the grant on the argument SHA — subsequent calls with different arguments will re-prompt

Silent-Skip Warning

If a request has no agent identity and the resolver keeps the default scope_to_agent=True, the grant is not persisted. The request still returns approved, but the next identical call re-prompts. The gateway logs:
Skipping scoped allow-always grant for tool 'shell_exec': no agent identity
on the request. Set scope_to_agent=False to grant globally.
Fix: set scope_to_agent=False to grant a request that has no agent identity.

Storage Layout

Grants live in one SQLite file — safe to back up, rotate, or delete.
AspectValue
Default path~/.praisonai/state/gateway/approvals.sqlite
Env overridePRAISONAI_HOME (whole state root moves)
Programmatic overrideExecApprovalManager(allowlist_path=...)
Journal modeWAL
Default TTL90 days (evicted lazily on read / list())
Safe to deleteYes — next restart re-prompts on the next request
If the store can’t be opened (permissions, disk full, corruption) the gateway logs and falls back to in-memory only — grants won’t survive restart, but the gateway does not crash.

HTTP Endpoint

GET/POST/DELETE /api/approval/allow-list manages grants over HTTP. POST and DELETE accept an optional agent_id; GET returns a grants view alongside the legacy allow_list.
curl -H "Authorization: Bearer $TOKEN" \
  http://localhost:8080/api/approval/allow-list
{
  "allow_list": ["read_file"],
  "grants": [
    {"agent_id": "shell-bot", "tool_name": "shell_exec", "arg_signature": null, "created_at": 1720000000.0, "approver": "gateway:human"},
    {"agent_id": "*", "tool_name": "read_file", "arg_signature": null, "created_at": 1720000000.0, "approver": "gateway:operator"}
  ]
}
Mutations (POST/DELETE) require OperatorScope.APPROVALS and pass the per-IP rate limiter. See Gateway Operator Scopes.

Legacy Behaviour Preserved

Still works. It creates an any-agent (agent_id="*") grant with an empty argument signature. "tool" in allowlist, allowlist.remove("tool"), and allowlist.list() also keep their original meaning.
Still works. Omitting agent_id creates an any-agent grant, exactly as before.
Set scope_to_agent=False on the Resolution. The grant then applies to every agent.
Delete the SQLite file, or call store.revoke_tool(name) to drop every grant for one tool across all agents.

Best Practices

One agent’s approval should never authorise another. The default scoping prevents accidental cross-agent leakage.
For shell_exec or apply_patch, add scope_to_args=True so the operator’s approval only covers the exact command they saw.
Rotate the SQLite file periodically, or shorten the window via ScopedAllowlistStore(ttl_seconds=...).
“Skipping scoped allow-always grant” means a resolver is sending scope_to_agent=True on a request with no agent identity — the grant was not persisted.
This is defence-in-depth, not a security boundary: a resolver that can approve requests can still choose scope_to_agent=False. The default scoping prevents accidental cross-agent leakage.

Gateway

Gateway overview and deployment

Approval Backends

How approval requests are routed

Gateway Operator Scopes

Authorisation for admin endpoints

Durable Approvals

Restart-safe bot pending-approval queue