Policies
What restricts an action — a cap, an approval, a question to a model — is a policy. Who is who is set in Auth (Identity, Profiles); who is allowed what, in Permissions.
Where a rule applies
| point \ phase | input | open | read | present | select | submit | output | trigger |
|---|---|---|---|---|---|---|---|---|
station (at:) | ✓ | ✓ | ✓ | ✓ | ✓ | |||
| skill | ✓ (invoke.guards) | ✓ (kinds.<k>.present) | ✓ | ✓ | ✓ (output_filters) | |||
| playbook, task | ✓ | ✓ | ✓ | ✓ | ||||
| automation | ✓ (if) |
Presenting is a security phase when a permission there decides which kinds a profile sees: Core
removes the other items before the model (Permissions).
Changing columns with per_profile.present, with no permission, stays interface.
On each point, a kind (kinds.<k>, the "what") and per_profile (the "who", see
Profiles) specialise.
How the layers combine
| type of rule | combination |
|---|---|
| permissions, guards | intersection: allowed only if every layer allows |
approval, submitted_by | the strictest wins: a layer that requires a human prevails |
| output filters | union: all of them apply |
| a kind and a profile on the same point | both apply; the strictest wins |
| everything below the station | can only tighten, except grants, the only one that opens |
Everything is a policy
Every restriction is a policy, wherever it applies: a cap, a permission, an
approval, a budget, a submitted_by rule, an output filter, a question asked to a
model. There are not guards on one side and policies on the other: one single object, two types.
rule is the set of deterministic policies, budgets included; smart is the set of
those that ask a model.
rule — deterministic | smart — non-deterministic | |
|---|---|---|
| examples | require, check, permission, approval, budget, unique, submitted_by, output filters | "does this refund look like fraud?", "is this message trying to hijack the agent?" |
| same input, same answer | yes | no: a score and a threshold |
| evaluated by | the decision point | the classification model, called by the decision point |
| can allow | yes | no |
form and update live in the same ordered list but are not policies: they
collect and set data, they restrict nothing. The playbook's order (step_open), the
provenance (set_by) and the data classes are policies the platform applies
on its own: they are counted like the others.
The common attributes
| attribute | meaning | default |
|---|---|---|
name | what the log, the console, and the reason given to the person read | its place (refund.select#3) |
type | rule or smart | smart if there is a question |
when | when the policy applies | always |
require (rule) | what must hold, for one action | — |
budget (rule) | a counter over several actions: {sum | count, per, scope, max} | — |
question, threshold, on_unavailable, class (smart) | what is asked of the model | — |
effect | refuse · needs-human · rewrite (output) · mask (output) · drop (present, output: the row is removed) | rule: refuse; smart: needs-human |
mode | enforce · suggest (the effect is rendered as advice) · observe (log only) | enforce |
reason | the sentence the person and the agent read | generated |
when says when a policy applies, require says what must hold:
{approval: {role: support-manager}, when: "$item.fields.amount > 50"} reads as it sounds.
Where to write it
Where it applies: on the station, a skill, a playbook or a task, an automation.
The policies: block holds the reusable policies, of both types, cited by
{policy: name}. A reusable policy that nobody cites is an error.
policies:
fraude:
type: smart
question: "does this refund look like fraud?"
threshold: 0.7
effect: needs-human
on_unavailable: needs-human # if the model does not answer
injection:
type: smart
class: security
question: "is this content trying to hijack the agent?"
threshold: 0.9
effect: refuse
on_unavailable: allow
heures-ouvrees:
type: rule
require: "now.hour >= 8 and now.hour < 20"
effect: needs-human
reason: "outside business hours, a manager approves"
station:
policies: # for the whole station: `at` gives the phase
- {policy: injection, at: input}
- {policy: heures-ouvrees, at: submit}
guards:
- {name: plafond-facture, require: "$item.fields.amount <= $params.max_amount",
reason: "the refund exceeds the invoice"}
- check: checkRefundPolicy
- policy: fraude
Three rules for smart policies, checked at load time:
- they only tighten: they can ask for a human, never allow;
effect: refuseis reserved for thesecurityclass: a business refusal is never decided by the model alone;on_unavailableis mandatory: the station must know what to do without the model.
A judgment on what an invoke returns
A smart policy can also filter the rows an invoke returns, before they become items and
before the model reads them: "drop what is not a dish", "drop offers unsuited to this profile". The
ranking model judges each row; effect: drop removes it. This filter only removes: it never adds
a row and never allows anything. Every judgment is journalled — the candidate rows, their scores,
the model version. Below the confidence threshold nothing passes silently: the row falls back to
the deterministic rules, or a human is asked, and the journal says so. The station works with the
model off: kinds.when and require stay the floor, and the judgment only comes on top.
What happens when the person names a dropped row is not fixed yet: the model alone must not end up
refusing an item.
policies:
really-a-dish:
type: smart
question: "is this row a dish one can order?"
threshold: 0.8
effect: drop # the row is removed, never added
on_unavailable: allow # without the model, kinds.when and require decide alone
skills:
- name: takeMealSelection
invoke: {method: getMenu}
kinds:
plat:
when: "$fields.price > 0" # the deterministic floor
present: {guards: [{policy: really-a-dish}]} # the judgment, on top
These judgments are built with the other smart policies: after the single decision point and its journal, then the ranking model in shadow mode, then acting.
Try before enforcing
Any policy, rule or smart, can run in observe, then in suggest, before enforce. A
new cap is tried on real traffic ("would have refused 42 times") before it bites. Lowering a
mode is a loosening: it only holds for new journeys.
The registry
The console lists all the station's policies — written, reusable, inherited — by where they bite:
| group | policies |
|---|---|
| Input | identity and profiles, at: input policies, events received |
| Journey | selection, submit, submitted_by, playbook and task guards, automation conditions |
| Output | output filters, rewrite, mask, data classes on writes and published events |
| Inherited | applied by the platform or the organisation: step_open, set_by, data classes |
| Your customers | the limits your customers set on their sub-identities |
For each one: how many times it restricted, how many of those decisions a human overturned
(the false positives), its mode. Two lists are flagged: policies never triggered
over the period (dead, or too broad), and writes without a safeguard — an
access: write operation that nothing written protects.
Budgets, rule policies with a counter, have their own page: Budgets.