Guides
Set Policies
Author policy rules — attribute matches, IP allowlists, method/endpoint restrictions, rate limits, time windows, operation and parameter rules, and approvals — from the dashboard or the CLI, plus non-stored per-request rules via an SDK constrained client.
By the end of this guide you can restrict what an agent or connection is allowed to do: deny requests by attribute, confine traffic to an IP range or an endpoint allowlist, cap its request rate, confine it to business hours, allow a provider operation only when its parameters look right (or redact fields / force a fresh sign-in), and hold sensitive calls for human approval — every rule type the policy engine supports.
A policy is a narrowing-only layer on top of a default-deny base. A request already needs ownership of the connection, the right scope, and a valid credential before any rule runs. A rule can only further restrict that base — creating one never grants or widens access. When a rule matches, it denies the request, rate-limits it, holds it for approval, or redacts part of it.
Where policies are authored
Section titled “Where policies are authored”| Surface | Use it for |
|---|---|
| Dashboard (developer portal) | Create, inspect, edit, toggle, simulate, and delete stored organization, application, provider, agent, and connection rules. |
| Wallet | End users manage account-wide and connection-specific rules and inspect disclosure-safe inherited controls. |
CLI (alter policy) | Scriptable application, provider, agent, and connection rules for CI and infrastructure-as-code. Organization rules stay dashboard-only; account rules stay Wallet-authored. |
| SDK constrained client | A non-stored rule carried by every credential request made through the returned with_constraints / withConstraints sub-client. |
| Connect popup (end users) | While authorizing an OAuth connection or delegating existing managed-secret access to an agent, the end user can set usage limits in the popup — the same self-scoped, narrowing-only rules available later in the Wallet. On by default; an application can hide the step with the Connect-session option (allowUserPolicyRules in the TypeScript SDK, allow_user_policy_rules in the Python SDK). |
Stored rules attach at six levels — organization, application, provider, agent, connection (grant), and account (user). End users can add connection rules in the Wallet or during Connect authorization. An SDK constrained-client rule can narrow calls without storing the rule. A request is evaluated against every applicable rule across the delegation chain, and a denial at any level wins.
View and understand a policy
Section titled “View and understand a policy”Select a policy anywhere it appears to open the shared detail viewer. Owned rules expand the complete definition: effect, target and source, operations, every parameter/operator/value condition, approval configuration, status, and timestamps. The developer portal’s Policies page indexes all six stored levels — organization, application, provider, agent, connection, and account. Account (user) rules are authored by the end user in the Wallet and shown read-only (disclosure-limited) in the portal; the Wallet’s Policy page separates the editable account-wide and connection-specific rules the end user owns from disclosure-safe inherited developer controls.
For a composed view, open a connection’s Policy tab in the Wallet: it groups the effective rules by account, an Inherited global group (organization + application), connection, provider, and agent. In the developer portal, the grant drawer’s Effective policy chain lists the same rules with organization and application as separate groups, and the Simulator tests a request shape and links every trace entry to the same full viewer.
Prerequisites
Section titled “Prerequisites”- An app with at least one agent or connection to attach a rule to.
- For the CLI, an authenticated
altersession (see the CLI reference).
Rate-limit an agent (quota)
Section titled “Rate-limit an agent (quota)”Goal: allow at most 1,000 requests per hour, counted per caller principal (the agent when the request runs as one, otherwise the connection).
Dashboard
Open the agent (or app), go to its Policies section, and click Add policy. Give it a name, choose Quota as the type, enter a limit of 1000, pick a period of hour, and click Create rule. The new rule card shows the 1000 / hour badge.
CLI
echo '{"limit": 1000, "period": "hour"}' | \ alter policy rules create --type quota --agent <agent-id> \ --body - --name "1000/hour per agent"Over the limit, the call is denied with a retry-after until the window resets (top of the minute/hour, UTC midnight, or the first of the month). This is the only rule type that responds with a rate-limit signal rather than a plain denial — the SDKs surface it as QuotaExceededError (see errors) and never auto-retry it.
Confine access to business hours (time window)
Section titled “Confine access to business hours (time window)”Goal: allow requests only Mon–Fri, 09:00–17:00, US Eastern.
Dashboard
In Policies → Add policy, choose the Time window type. Toggle the days Mon–Fri, set start 09:00 and end 17:00, and enter a timezone of America/New_York. Click Create rule.
CLI
echo '{ "windows": [{"days": ["mon","tue","wed","thu","fri"], "start": "09:00", "end": "17:00"}], "timezone": "America/New_York"}' | alter policy rules create --type time_window --agent <agent-id> \ --body - --name "Business hours"The request is denied unless its local time (in the required timezone) falls inside a window. A window is [start, end) — start inclusive, end exclusive; use "24:00" for end-of-day. A start later than end is a valid overnight window (e.g. fri 22:00 → sat 06:00). Up to 20 windows per rule; the timezone is required.
Deny requests by attribute (match rule)
Section titled “Deny requests by attribute (match rule)”Goal: make an application read-only — deny every POST, PUT, PATCH, and DELETE, app-wide.
Dashboard
Open the app’s Policies section and click Add policy. Choose the Request match (deny) type, add a condition with the attribute method and the values POST, PUT, PATCH, DELETE, and click Create rule.
CLI
echo '{"when": {"method": ["POST", "PUT", "PATCH", "DELETE"]}, "effect": "deny"}' | \ alter policy rules create --body - --name "Read-only app"A match rule denies when ALL of its conditions match; a condition value is an exact string or a list (membership). Conditions can name request metadata (method, provider_id, agent_id, client_ip, …) or the classified operation vocabulary — operation (a catalog operation id) and family (send, read, write, delete, admin, payment) — so {"when": {"family": ["payment"]}, "effect": "deny"} blocks everything that moves money. Two fail-closed behaviors to know: a method condition also gates raw-token retrievals (a raw token confers every method), and an operation/family condition also fires on requests that cannot be classified. client_ip is an exact-IP match — for ranges, use an IP allowlist rule instead.
Confine traffic to an IP range (IP allowlist)
Section titled “Confine traffic to an IP range (IP allowlist)”Goal: only accept requests originating from the office egress range.
Dashboard
In Policies → Add policy, choose the IP allowlist type and enter one address or CIDR range per line — for example 203.0.113.5 and 10.0.0.0/8. Click Create rule.
CLI
echo '{"allow": ["203.0.113.5", "10.0.0.0/8"]}' | \ alter policy rules create --type ip_allowlist --body - --name "Office egress only"Requests from any source IP not on the list are denied. A /0 catch-all — or a set of entries that together cover the whole address space — is rejected at authoring: an allowlist that matches every IP is not a restriction. Multiple IP allowlist rules at different levels compose by intersection (a request must satisfy all of them), and a request whose source IP cannot be determined is denied, never waved through. This is an operator rule type (dashboard/CLI); end users cannot author it from the Wallet.
Limit methods and endpoints (restriction)
Section titled “Limit methods and endpoints (restriction)”Goal: confine a connection to read-only repository access.
Dashboard
In Policies → Add policy, choose the Method/endpoint restriction type. Enter the allowed methods (GET) and the allowed endpoint paths (/repos/**), and click Create rule.
CLI
echo '{"allowed_methods": ["GET"], "allowed_endpoints": ["/repos/**"]}' | \ alter policy rules create --type restriction --grant <grant-id> \ --body - --name "Read-only repos"A request is denied unless its method is in allowed_methods (when set) AND its provider API path matches allowed_endpoints (when set) — at least one of the two must be present. An endpoint is an exact path (/user) or a trailing /** prefix wildcard (/repos/** matches /repos and everything under it); no other wildcard form exists. Restrictions are enforced on proxied calls, so a connection carrying one refuses raw-token retrieval — a raw token could not be held to the allowlist after handoff. Like the IP allowlist, this is an operator rule type.
Restrict a specific operation (content rule)
Section titled “Restrict a specific operation (content rule)”Content rules bind reviewed provider operations — or whole semantic families (send, read, write, delete, admin, payment) — from the operation catalog, optionally narrowed by conditions over the request’s parameters. They have three effects: deny, redact (strip named fields before the call), or step_up (require a recent end-user sign-in).
Deny by parameter — outbound email to internal recipients only
Section titled “Deny by parameter — outbound email to internal recipients only”Goal: allow Gmail send only when every recipient is @acme.com.
Dashboard
In Policies → Add policy, choose the Content rule type. Pick the provider (Google), search the catalog and select the operation (gmail.users.messages.send). Click Add condition, set the parameter name to recipients, the operator to not all within, and the value to *@acme.com. Leave the effect on Deny and click Create rule.
CLI
echo '{ "match": {"operations": ["gmail.users.messages.send"]}, "params": [{"name": "recipients", "op": "not_subset_of", "value": ["*@acme.com"]}], "effect": "deny"}' | alter policy rules create --type content_match --provider google \ --body - --name "Internal recipients only"Condition operators are equals, any_in (fires if any value matches), not_subset_of (fires when values are NOT fully covered by the allowlist — the recipient-allowlist shape), and the numeric gt/gte/lt/lte. List values support per-entry globs (*@acme.com).
Redact instead of deny
Section titled “Redact instead of deny”Swap the effect to strip fields before the call is forwarded (the call is refused if the redaction cannot be provably applied):
echo '{ "match": {"operations": ["gmail.users.messages.send"]}, "effect": "redact", "redact": {"fields": ["subject"]}}' | alter policy rules create --type content_match --provider google \ --body - --name "Strip subject on sends"Require a fresh sign-in (step-up)
Section titled “Require a fresh sign-in (step-up)”echo '{ "match": {"operations": ["gmail.users.messages.send"]}, "effect": "step_up", "step_up": {"max_session_age_seconds": 300}}' | alter policy rules create --type content_match --provider google \ --body - --name "Recent sign-in to send"Hold sensitive calls for a human (require approval)
Section titled “Hold sensitive calls for a human (require approval)”Goal: pause every write for an approver to review.
echo '{ "effect": "require_approval", "when": {"method": ["POST", "DELETE"]}, "approval": {"approvers": ["lead@acme.com"], "channels": ["email"]}}' | alter policy rules create --type require_approval --agent <agent-id> \ --body - --name "Approve writes"In the dashboard this is the Require approval type on the same Add policy form. The end-to-end approval flow — how the request pauses and how the application receives the outcome — is covered in Add Human-in-the-Loop Approvals.
One rule produces one approval gate and currently designates the first address in its approver list. For multi-party N-of-N approval, author distinct applicable rules with distinct approver sets; Alter combines them into separate gates and requires every gate to approve.
The when condition also accepts the content-rule vocabulary — operations/families plus parameter conditions — so approval can hinge on what the request carries, not just its shape:
# Only payments over 1,000 need sign-offecho '{ "effect": "require_approval", "when": { "match": {"families": ["payment"]}, "params": [{"name": "amount", "op": "gt", "value": 1000}] }, "approval": {"approvers": ["cfo@acme.com"]}}' | alter policy rules create --type require_approval --body - --name "Approve large payments"Omit when entirely to require approval on every request at the target. An operation-scoped approval condition on a request that cannot be classified is denied — it is never silently approved around.
How rules combine
Section titled “How rules combine”Every applicable rule — at every level, across the whole delegation chain — is evaluated on every request, and they compose as AND: a request must satisfy all of them, and a denial at any level wins. There is no rule ordering to reason about and no way for one rule to override another, because no rule can widen access — each one only narrows. So a stack like:
- app-wide:
time_window(business hours) - agent:
quota(1,000/hour) - provider:
content_match(internal recipients only) - connection:
require_approval(writes need sign-off)
means a request must be inside the window AND under the quota AND pass the content conditions, and a matching write still waits for its approval. Multiple windows inside ONE time-window rule are a union (any window admits); multiple time-window rules across levels intersect (all must admit).
Test before you commit (simulate)
Section titled “Test before you commit (simulate)”Dry-run a request against the live rules without making a real call:
alter policy simulate --grant <grant-id> \ --operation gmail.users.messages.send \ --params '{"recipients": ["someone@gmail.com"]}'The output shows the verdict, the per-rule trace, and which operation the request classified to. The dashboard has the same policy explorer.
Attach a non-stored rule with a constrained SDK client
Section titled “Attach a non-stored rule with a constrained SDK client”Stored rules are the durable controls. with_constraints / withConstraints returns a sibling client whose credential requests all carry the additional rule. Use that sibling for constrained calls and retain the original client for calls that should not carry the rule:
from alter_sdk import content_match_rule
# Require a recent end-user sign-in for calls through this sibling:fresh = app.with_constraints( rule=content_match_rule( operations=["gmail.users.messages.send"], effect="step_up", max_session_age_seconds=300, ),)await fresh.proxy_request( "POST", provider_url, grant_id=grant_id, reason="Send a reviewed message",)import { contentMatchRule } from "@alter-ai/alter-sdk";
const fresh = app.withConstraints({ rule: contentMatchRule({ operations: ["gmail.users.messages.send"], effect: "step_up", maxSessionAgeSeconds: 300, }),});await fresh.proxyRequest({ method: "POST", url: providerUrl, grantId, reason: "Send a reviewed message",});The builders validate the rule locally (effect-specific requirements included) before it is sent.
Manage existing rules
Section titled “Manage existing rules”alter policy show-app # app-level rules at a glancealter policy rules list --agent <agent-id> # list rules at a levelalter policy rules update --rule <rule-id> --disable # toggle without deletingalter policy rules delete --rule <rule-id> # remove