Limits: policies, rules and approvals
The limits of policies, sqlguard, budgets, policy versions, the rules that run each call, approvals, lockdown mode, webhooks and scheduled actions.
Scheduled actions and outbound mail (S40)
Section titled “Scheduled actions and outbound mail (S40)”- A schedule is one window per action: no recurrence, no proxy mode, at most 72 hours ahead.
- A missed window is cancelled and sealed, never caught up. If the server is down for the whole window, the action is cancelled when it comes back.
outbound_mailexists only in the ops build and only for listed orgs. It sends plain text from one mailbox, one recipient at a time, and never retries; a lost answer is UNVERIFIABLE unless the read-back finds the message by its Message-ID.- Whether Gmail’s DKIM signature covers the two unsubscribe headers depends on the Workspace’s DKIM setup; Deedglass sets the headers but can’t sign them.
Policy limits (S10)
Section titled “Policy limits (S10)”What policies can’t do (POLICY_LANGUAGE.md section 12):
- No loops and no functions of your own. CEL has bounded comprehensions (
all,exists,map,filter), nested at most two deep, and in practice a loop inside a loop overargsis over the cost limit. - No network lookups, files or clocks. Everything a rule can read is declared; the time is an input recorded with the action.
- No state beyond
usage. A rule can’t remember the previous action; budgets and the usage counters are the only memory. - No composition across files in v1. One enforce policy is active per org; there is no
include:. - CEL doesn’t understand SQL. The
sqlfacts come from S11’s classifier, which is conservative and is not a full Postgres parser. When it isn’t sure it saysambiguous. - The cost estimate is an estimate. It uses fixed size hints; the runtime cost limit is the backstop for larger real values.
rule_unreachableis conservative. It only warns when coverage is certain from thematchfields of a rule withoutwhen; it never reasons about conditions, so some dead rules go unflagged.secret_in_policyknows eight prefixes. Other secrets pasted into a policy are not detected.fmtmoves some comments. Comments stay in the file, but a comment inside amatchorapprovalkeeps that mapping in block style, and a comment between keys moves with the key that follows it.
Policy evaluation, sqlguard and budgets (S11)
Section titled “Policy evaluation, sqlguard and budgets (S11)”- Cost-based budgets can overshoot. A
max_spendbudget without ameasure, and an agent’sspend_per_day, are checked before execution and counted after the tool reports its cost, so actions already in flight can pass the limit (BUDGETS.md section 7). Call budgets andmeasurebudgets are exact across instances. - No FX. A budget counts only costs in its own currency; there is no conversion. An agent’s
spend_per_daycompares usage in the snapshot’s currency only. - Static SQL analysis can’t see what Postgres runs on your behalf. Triggers, rules, function
bodies,
DObodies and settings changed earlier in a session are invisible to sqlguard. The L2 witness sees what Postgres actually changed (SQLGUARD.md section 12; WITNESS.md). - sqlguard is conservative, so some valid SQL is refused as ambiguous: a backslash in a plain
'…'string, non-ASCII identifiers outside quotes,BEGIN ATOMICfunction bodies, and any statement it doesn’t recognise. A string with several statements counts as at leasttransaction, neverread. where_trivialis best effort. It catchesWHERE true,WHERE 1 = 1and a few similar shapes, not every condition that is always true.tablesis best effort.tables_completeis false for functions inFROM,LATERAL, three-part names and DDL on things that aren’t tables; a policy that allow-lists tables should require it.- Very long lists in
argshit the time limit before the cost limit. cel-go v0.32.0’s runtime cost tracking grows quadratically with comprehension length, so for a list of tens of thousands of items the 50 ms evaluation deadline stops the evaluation before the cost limit does. Either way the action is denied (policy_unavailableorpolicy_denied), never allowed. - A 50-rule policy where every condition runs allocates about 630 objects per evaluation, over the target of 200. The realistic postgres-safe policy allocates about 170.
Policy versions, activation and the registry (S12)
Section titled “Policy versions, activation and the registry (S12)”- One enforced policy and at most one shadow per org. No includes, no per-agent policies (use
match.agentand labels), no scheduled activations. - The activation deed check trusts the published seal keys. On every load the registry
recompiles the stored YAML, compares both hashes, and verifies the activation deed against the
keys in
deedglass.seal_keys(the same setkeys.jsonpublishes). Someone with superuser access who also registers a seal key of their own and appends a deed under it would pass that check; the new key would show inkeys.json, anddeedglass verifyagainst your own copy of the key set would refuse those deeds. Keep a copy ofkeys.jsonoutside Deedglass. version_hashis frozen per language version. It is computed over the normalized policy. A future build that normalized differently would see every stored version as changed and refuse them (fail closed). The normalizer can’t change withindeedglass.policy/v1;hashes.jsonand S10’s lint goldens pin it.- Shadow decides nothing. The registry loads the shadow version and
EvaluatePairdecides with it; since S13 the Gate records that decision on each action (decision.shadow), but nothing compares or reports shadow results until S27 (Insights). - A shadow version that fails its checks is dropped, not enforced against. It is logged and counted; only an enforced version that fails makes the org refuse every action.
- The dry run reads usage as zero unless the request gives it; it never reserves. Real counters are read by the Gate (S13).
- NOTIFY is best effort; the poll is the backstop. A lost notification is caught by the 30 s
safety poll, and a reconnect reloads everything. Within those windows an instance can serve the
previous version for up to 30 s if Postgres drops a notification; the measured hot swap is about
2 ms when it doesn’t (
bench/results/2026-10-01-darwin-arm64-policy-registry.json). - Rate limits on lint, evaluate and test are per instance (60 a minute per person or key, in memory), like S07’s limiter.
policy diffandpolicy replayaren’t built (S27). The Console editor is S26.
Gate limits (S13)
Section titled “Gate limits (S13)”What the Gate doesn’t do (GATE.md):
- A proxy-mode result is the edge’s word. In proxy mode the edge runs the tool and reports the
result; the deed says
edge_reportedand records what the edge said. Deedglass can’t tell a truthful edge from one that lies. The L2 witness checks Postgres on its own (gateway mode). - L0 means the tool reported success. It is the tool’s own answer (method
reported), not an independent check. Without a read-back (S19) or L2, a verdict stays at L0. - Timeouts and crashes become UNVERIFIABLE: it may have run. When a tool doesn’t answer within
gate.execute_timeout, the edge never reports, or the server stops while a tool runs, the action is sealed withexecution.status: unknownand the verdict UNVERIFIABLE. The deed says plainly that the tool may have run; check the target system by hand. - No retries. The Gate never calls a tool a second time. A timeout is an unknown outcome, not a reason to try again, and a retry with the same Idempotency-Key replays the first answer.
- It doesn’t undo a tool that ran. Stop and cancel refuse what hasn’t run; S23 adds undo where a connector supports it.
dg_test_keys aren’t blocked. The action recordslivemode(and so does its context), but connectors have no test or live mode in v1, so a test key can reach a live system. Enforcement waits for connector modes (v0.2).- The fake connector is for development only.
fake.echo,fake.write,fake.failandfake.slowexist only whenDEEDGLASS_ENV=dev(and in tests). Real connectors arrive in S16 and later. - A gateway action keeps running when its HTTP request ends. The tool call is detached from
the request: a client that stops waiting, or a
waitthat ends, gets 202processing, and the action is still recorded and sealed. Closing the connection doesn’t cancel the tool. - 20 open waits per agent, counted per instance. Each agent may hold 20 open
GET /v1/actions/{id}?wait=requests on one server; the 21st is 429rate_limited. Behind N instances an agent can hold up to N times as many. - The Idempotency-Key comparison leaves out the signed intent. The fingerprint is the request
digest plus
Deedglass-On-Behalf-OfandDeedglass-Session, so a retry signed again still replays, and a replay doesn’t check the new intent. The original action’s intent was checked when it was admitted. - An Idempotency-Key lasts 24 hours. After that the key is free again, and the same request with the same key creates a new action.
Approvals and webhooks (S22)
Section titled “Approvals and webhooks (S22)”- An approval link is a bearer token for one person and one approval. Whoever holds the email
can use the link until it is used, voided or expires (at most 24 hours, never past the
approval). The token is in the URL fragment, so it doesn’t reach logs, proxies or referrers, but
a forwarded email forwards the link. Orgs that want a session for every decision set
email_links: review. - The page shows the arguments (with secrets scrubbed) to whoever opens a valid link, so the
person sees what they countersign.
email_detail: summarykeeps them out of the email itself. - The row estimate is the planner’s, from table statistics: “about 1 row” can be wrong, which is why the Witness still compares what actually changed.
- The policy isn’t evaluated again at approval time. A rule changed while an approval waits applies to the next action.
- Webhook payloads are thin: ids and states. Receivers fetch the object for anything else.
- Webhook deliveries are at least once and unordered. Dedupe by
Deedglass-Event-Id; a retry can arrive after a later event. - A disabled webhook endpoint stays disabled. After 72 hours of failures it is turned off; create it again to resume. Events in between aren’t replayed.
- Email only. Approvals notify by email in S22; Slack arrives with S29, MCP tasks later.
Lockdown mode
Section titled “Lockdown mode”Off unless lockdown: true is in the config or an org turned it on (LOCKDOWN.md, ADR-117).
- The server CLI can’t tell who is typing.
deedglass lockdown on|off|ackrun on the server as the operator and record the people named with--asas given. Anyone with the server’s shell and database URL can name two people for the turn-off, or change the database directly. - No Console screen and no API route. Lockdown is turned on and off from the server CLI or the config; turning off the config key isn’t a sealed record.
- The SQL rules cover the native Postgres connector only. SQL sent through other tools isn’t classified; the person approving sees it.
- Proven only where the connector reaches it. Other writes are checked at Checked or Claimed, and the record says so.
- High risk is a fixed list (destructive tools, deletes, database functions, undo, Stripe). A Can’t check hold is released by one person.
- No outside timestamps without TSAs. Lockdown keeps export timestamps on but adds no TSA and no schedule.
More limits
Section titled “More limits”- Limitations: the known limits in v1, and every part
- Limits of sign-in, agent keys, Stop and intents
- Limits of the MCP wrapper, gateway, A2A and demo