Skip to content

Policy language

A SASY policy answers a question about a proposed action: may this authenticated caller perform it, given the recorded context? Policies use Soufflé Datalog in .dl files. The engine supplies the common types and relations; your file adds rules.

A relation is a table of facts. Actions contains the proposed actions, Principal identifies the authenticated caller, and Edge records message dependencies. These supplied facts are sometimes called EDB, or input relations. A relation computed by rules is called IDB, or a derived relation.

IsAuthorized(idx) :- Actions(idx, action), IsTool(action, "Read").

Read this as: “put idx on the allowlist if that action is a Read tool call.” Commas mean AND. Separate rules with the same head mean OR: either rule can produce the fact. Variables join matching values across relations; _ means any value, without retaining a binding. Quoted strings are constants. Rules end with a period.

A request can contain several actions. The first Actions column is their zero-based index; authorization and denial results use the same index. For example, the engine might supply this fact:

Actions(0, $CallTool("Read", "{\"file_path\":\"notes.txt\"}")).

This illustrates an input fact, not something to hard-code in a policy. $CallTool is a tagged value with a tool name and JSON arguments. The common action type also includes $HTTPRequest(url, body, headers_json) and $SendAttempt(message). @json_get_str(args, "file_path") can extract a string from tool arguments.

Save this as read-only.dl:

// Allow Read calls. Other tool names have no allowlist match.
IsAuthorized(idx) :- Actions(idx, action), IsTool(action, "Read").
// @deny_message: Writes are disabled in this session
// @suggestion: Read the existing file and describe the proposed change
Unauthorized(idx) :- Actions(idx, action), IsTool(action, "Write").

Validate it locally with make souffle-validate FILE=read-only.dl, then bind it to a session as shown in configuration. This policy checks tool names; it does not restrict which files Read may access. The application must check the actual proposed action before executing it.

The common authorization rule requires an authenticated principal, an IsAuthorized(idx) match, and no Unauthorized(idx) match. A hard denial wins even if an allow rule also matches. With no allowlist match, the action is not authorized. A new engine store starts with a deny-all bootstrap policy; choosing an allow-by-default policy is an explicit policy decision.

Use HasRole("role-name") to require a role associated with the authenticated principal. Principal and PrincipalRole come from authentication. Entity is the caller-supplied actor label and is not proof of identity. See authentication.

Edge(source, destination) means destination depends on source. If a tool result influenced an assistant message, the edge points from the result to that message.

Current(id) marks the context nodes supplied with the check. CurrentDepends(id) finds their ancestors by following dependency edges backward. It does not include a current node merely because it is current. A policy that needs both can define:

.decl InScope(id: symbol)
InScope(id) :- Current(id).
InScope(id) :- CurrentDepends(id).

Join these IDs to ToolResult(id, tool_name, args) or SentMessage(id, message) to inspect recorded evidence. This describes what the application recorded; missing observations do not prove that an event never happened. !Relation(...) tests absence in the supplied facts and derived results, so policies using absence depend on complete recording for the question they ask.

The Python preprocessor, sugar.py, expands supported shorthand and rewrites ordinary Unauthorized rules into DenialReason(idx, kind, reason, suggestion) rules. Without annotations, the reason is Action is denylisted, the suggestion is empty, and the kind is block. The common rule derives hard denial from block reasons. // @ask instead produces an approval request; it needs an application’s approval handling and does not override a hard block. Annotations on IsAuthorized remain author hints, not denial rules.

Graph observations belong to the authenticated tenant and session. Each check supplies its current nodes, action batch, identity and per-action ActionMetadata; PolicyMetadata supplies configuration bound with the policy. Reusing a session preserves its recorded context, while a new session has its own graph and policy binding. Recording a fact does not execute or authorize a tool.

Allow-route diagnostics can identify incompatible fixed request conditions. possible means only that necessary conditions passed; it does not prove that a suggested change will authorize the action. Custom approval predicates remain changeable. Automatic ancestry gates can skip unnecessary graph work, but preserve authored gates and fall back conservatively on unsupported syntax or exhausted analysis limits. Evaluation deadlines still apply. See limits for the boundary between these diagnostics, static analysis and runtime enforcement.