Skip to content

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.

  • 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_mail exists 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.

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 over args is 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 sql facts come from S11’s classifier, which is conservative and is not a full Postgres parser. When it isn’t sure it says ambiguous.
  • The cost estimate is an estimate. It uses fixed size hints; the runtime cost limit is the backstop for larger real values.
  • rule_unreachable is conservative. It only warns when coverage is certain from the match fields of a rule without when; it never reasons about conditions, so some dead rules go unflagged.
  • secret_in_policy knows eight prefixes. Other secrets pasted into a policy are not detected.
  • fmt moves some comments. Comments stay in the file, but a comment inside a match or approval keeps 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_spend budget without a measure, and an agent’s spend_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 and measure budgets are exact across instances.
  • No FX. A budget counts only costs in its own currency; there is no conversion. An agent’s spend_per_day compares usage in the snapshot’s currency only.
  • Static SQL analysis can’t see what Postgres runs on your behalf. Triggers, rules, function bodies, DO bodies 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 ATOMIC function bodies, and any statement it doesn’t recognise. A string with several statements counts as at least transaction, never read.
  • where_trivial is best effort. It catches WHERE true, WHERE 1 = 1 and a few similar shapes, not every condition that is always true.
  • tables is best effort. tables_complete is false for functions in FROM, 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 args hit 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_unavailable or policy_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.agent and 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 set keys.json publishes). 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 in keys.json, and deedglass verify against your own copy of the key set would refuse those deeds. Keep a copy of keys.json outside Deedglass.
  • version_hash is 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 within deedglass.policy/v1; hashes.json and S10’s lint goldens pin it.
  • Shadow decides nothing. The registry loads the shadow version and EvaluatePair decides 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 diff and policy replay aren’t built (S27). The Console editor is S26.

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_reported and 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 with execution.status: unknown and 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 records livemode (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.fail and fake.slow exist only when DEEDGLASS_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 wait that ends, gets 202 processing, 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 429 rate_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-Of and Deedglass-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.
  • 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: summary keeps 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.

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|ack run on the server as the operator and record the people named with --as as 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.