Policy
Policy: which tools may run, which need a person's approval.
noclick.Policy.set(deny=["*.delete_*"], require_approval=["gmail.send_email_message"])
noclick.Policy.set(allow=["search", "gmail.*"], user="user_42") # narrows the account policyPatterns match dotted tool names (gmail.*, *.delete_*, search).
Deny wins; an empty allow allows everything the other rules don't stop.
Like every call, get, set and evaluate follow as_user: inside it they
address that user's policy, and user=None addresses the account's. A rule can
also be a noclick.Rule with a condition on the call's arguments and a person
who approves it.
class PolicyThe account's policy, or one user's (user_id). Each list holds patterns,
and Rules where a rule has a condition or an approver.
| allow | list[str | Rule] | |
| deny | list[str | Rule] | |
| require_approval | list[str | Rule] | |
| user_id | str | None | |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |
listclassmethodasync
noclick.Policy.list(*, client=None) -> list[Policy]List the account's policy and every user policy.
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)The account's policy first (its user_id is None), then each user that has a policy of
its own, with your id for that user as its user_id.
for policy in noclick.Policy.list():
print(policy.user_id or "account", policy.deny, policy.require_approval)evaluateclassmethodasync
noclick.Policy.evaluate(
tools, *, arguments=None, user=..., draft=None, client=None
) -> list[PolicyVerdict]Judge each of tools (dotted or wire names, at most
500) against the policy for the account, or user= (inside as_user, that user;
user=None for the account). A draft
({"layer": "account"|"user", "allow", "deny", "require_approval"})
is judged in place of its layer's saved rules. Nothing runs and no
approval is requested. arguments judges one tool's call with those
arguments, so rules with a when apply; without them a rule with a
when never matches.
NoneWho the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
NoneThe client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)One verdict per tool, in order: allow, deny or approval, with the matched_pattern
and layer that decided it, and the whole rule. matched_pattern is None when no rule
matched, or when an allow list left the tool out.
See how a user's calls would be judged:
for verdict in noclick.Policy.evaluate(["gmail.send_email_message", "search"], user="user_42"):
print(verdict.tool, verdict.verdict, verdict.matched_pattern)Try rules before you save them:
verdicts = noclick.Policy.evaluate(
["gmail.send_email_message", "gmail.fetch_emails_from_inbox"],
draft={"layer": "account", "deny": ["gmail.*"]},
)See which rule a particular call meets:
[verdict] = noclick.Policy.evaluate("stripe.create_refund", arguments={"amount": 25000}, user="user_42")
print(verdict.verdict, verdict.rule) # approval Rule(tool='stripe.create_refund', when='args.amount > 10000', ...)getclassmethodasync
noclick.Policy.get(user=..., *, client=None) -> PolicyRead the account policy, or with user= (a User or your id for
one, and inside as_user that user) that user's own rules; user=None
reads the account's.
Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)The allow, deny and require_approval lists, empty where nothing is set. A user's
policy holds only its own rules, not the account's.
policy = noclick.Policy.get()
print(policy.allow, policy.deny, policy.require_approval)
ana = noclick.Policy.get(user="user_42")setclassmethodasync
noclick.Policy.set(
*, allow=(), deny=(), require_approval=(), user=..., client=None
) -> PolicyReplace the account policy (or user='s, and inside as_user that user's;
user=None is the account's) with these rules: patterns, and noclick.Rules
(or dicts) with a condition or an approver.
()()()Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)The saved policy, its patterns lowercased and deduplicated. A list you left out is now empty.
Pass every rule you want to keep, since the call replaces the whole policy:
noclick.Policy.set(deny=["*.delete_*"], require_approval=["gmail.send_email_message", "stripe.*"])A user's policy narrows the account's:
noclick.Policy.set(allow=["search", "gmail.*"], user="user_42")addclassmethodasync
noclick.Policy.add(*rules, to='require_approval', user=..., client=None) -> PolicyAdd rules to one list of the account's policy (or user='s; inside as_user
that user's), leaving every other rule as it is, in one change on the server. A rule
for a tool that list already has replaces it, so code that owns a rule can add it
on every run. to is "require_approval", "deny" or "allow".
'require_approval'Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)The saved policy, with the rest of its rules.
Own one rule from the code that needs it, safe to run on every deploy:
noclick.Policy.add("quickbooks.create_bill")Deny an operation for one user:
noclick.Policy.add("slack.*", to="deny", user="user_42")Hold only large bills, for finance to approve:
noclick.Policy.add(
noclick.Rule("quickbooks.create_bill", when="args.total_amount > 2500", approver="finance@acme.com"),
)removeclassmethodasync
noclick.Policy.remove(*tools, user=..., client=None) -> PolicyRemove the rules for these tools, as written ("stripe.create_refund",
"gmail.*"), from every list of the account's policy (or user='s; inside
as_user that user's), leaving the rest. A tool with no rule changes nothing.
Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)The saved policy, with the rules that are left.
noclick.Policy.remove("quickbooks.create_bill")PolicyVerdict
class PolicyVerdictWhat a policy decides for one tool (allow, deny or approval),
and which rule (matched_pattern in layer account/user) decided it.
| tool | str | |
| verdict | str | |
| matched_pattern | str | None | |
| layer | str | None | |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |
| rule | str | Rule | None | The rule that decided, whole: a pattern, or a |
Rule
class RuleA policy rule that says more than a pattern: tool is the pattern
("stripe.create_refund", "gmail.*"), when a CEL expression the call
must satisfy for the rule to apply, over args (the call's final arguments,
bound ones included) and user (the user it's made for: id, email,
name, metadata), and approver who approves it in
require_approval: a Human (user.human, noclick.Human(email=...),
or noclick.Human() for the user the call is made for) or an email; it's emailed
the approval, and only it may answer. Without an approver the owner of what the
call acts in approves, as for a pattern. A rule goes in any of the three lists; a
dict with the same keys works too.
message, on a deny rule, is what the call's refusal says in your words, so the
agent can tell the user what to do: "Web search comes with Pro:
https://acme.com/upgrade". It may use {{user.id}} and the other user fields, as
pause_spending does, filled in for whom the call is for; a field they lack leaves the
refusal its usual words.
A call when can't judge (an argument it lacks, values that don't compare) meets
a deny or require_approval rule and misses an allow one, so a condition
never opens what it was written to close; has(args.amount) && ... is precise.
Numbers compare within their own type: write double(args.amount) > 99.5 for an
amount that may have a fraction.
Refunds over $100 wait for finance; smaller ones run:
noclick.Policy.add(
noclick.Rule("stripe.create_refund", when="args.amount > 10000", approver="finance@acme.com"),
to="require_approval",
)A plan feature, refused with what to do about it:
noclick.Policy.add(
noclick.Rule("search", when='user.metadata.plan == "free"',
message="Web search comes with Pro: https://acme.com/upgrade?user={{user.id}}"),
to="deny",
)Never email a competitor, and let pro users' agents book without asking:
noclick.Policy.set(
deny=[noclick.Rule("gmail.send_email_message", when='args.to.exists(t, t.endsWith("@rival.com"))')],
require_approval=[noclick.Rule("calendar.*", when='user.metadata.plan != "pro"')],
)| tool | str | |
| when | str | None | |
| approver | Any | |
| message | str | None | What a |