Skip to content

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.

SurfaceUse it for
Dashboard (developer portal)Create, inspect, edit, toggle, simulate, and delete stored organization, application, provider, agent, and connection rules.
WalletEnd 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 clientA 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.

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.

  • An app with at least one agent or connection to attach a rule to.
  • For the CLI, an authenticated alter session (see the CLI reference).

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

Terminal window
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

Terminal window
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.

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

Terminal window
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 vocabularyoperation (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

Terminal window
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.

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

Terminal window
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

Terminal window
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).

Swap the effect to strip fields before the call is forwarded (the call is refused if the redaction cannot be provably applied):

Terminal window
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"
Terminal window
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.

Terminal window
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:

Terminal window
# Only payments over 1,000 need sign-off
echo '{
"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.

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).

Dry-run a request against the live rules without making a real call:

Terminal window
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.

Terminal window
alter policy show-app # app-level rules at a glance
alter policy rules list --agent <agent-id> # list rules at a level
alter policy rules update --rule <rule-id> --disable # toggle without deleting
alter policy rules delete --rule <rule-id> # remove

Report an issue with this page

Necessary

Required for sign-in, security, authorization, and remembering your choices.

Always active

Analytics

Helps us understand which product and documentation features are useful.

Performance diagnostics

Uses performance tracing and privacy-masked session replay to diagnose problems.

You can change these choices at any time from Cookie settings.