Docs

Rules, amounts and currencies

How a policy document is structured, how rules match calls, how amounts and currencies are read, and how approvers and templates work.

A policy version is one JSON document in the kblock.cloud/policy@1 format. It holds a list of rules and a default. For every tool call, the Gateway evaluates the agent's active version: the rules that match the call compete, the most restrictive one wins, and when no rule matches, the default decides.

This page is the reference for writing that document. To create and save one, see Create a policy.

A complete example

This document is valid (it was checked with K-Block's own validator) and is a simplified version of the Support refunds template. It assumes tools whose actions are orders.read, orders.search, refund.issue, payouts.send and customers.export, and a refund tool whose amount is mapped in USD.

policy.json
{
  "schema": "kblock.cloud/policy@1",
  "default": "deny",
  "rules": [
    { "id": "read-orders", "actions": ["orders.read", "orders.search"], "effect": "allow" },
    {
      "id": "refund-over-limit",
      "actions": ["refund.issue"],
      "amount": { "currency": "USD", "gt": "1000" },
      "effect": "deny"
    },
    {
      "id": "refund-approval",
      "actions": ["refund.issue"],
      "amount": { "currency": "USD", "gt": "100", "lte": "1000" },
      "effect": "require_approval",
      "approvers": { "roles": ["owner", "admin", "approver"] },
      "approval_ttl_seconds": 14400
    },
    {
      "id": "refund-auto",
      "actions": ["refund.issue"],
      "amount": { "currency": "USD", "lte": "100" },
      "effect": "allow"
    },
    { "id": "never", "actions": ["payouts.*", "customers.export"], "effect": "deny" }
  ]
}

How it decides some calls:

CallDecisionDeciding rule
orders.readallowread-orders
refund.issue, 40 USDallowrefund-auto
refund.issue, 100 USDallowrefund-auto (lte includes 100)
refund.issue, 100.01 USDrequire approvalrefund-approval, request open up to 4 hours
refund.issue, 1500 USDdenyrefund-over-limit
refund.issue, no readable amountdenyrefund-over-limit (fail closed, see Missing amounts)
payouts.senddenynever
crm.contacts.mergedenynone: the deny default

Document structure

MemberRequiredValue
schemaYesExactly "kblock.cloud/policy@1".
defaultYes"deny" or "require_approval". An "allow" default is refused (PERMISSIVE_DEFAULT).
rulesYesAn array of at most 200 rules. It may be empty.

Validation is strict: any member not listed on this page, at any level, is refused with UNKNOWN_MEMBER.

Rule members

MemberRequiredValue
idYesUnique in the document. Lowercase letters, digits, _ and -, starting with a letter or digit, at most 64 characters.
actionsYes1 to 64 action patterns (see Matching).
effectYes"allow", "require_approval" or "deny".
toolsNo1 to 64 exposed tool names. When present, the rule only matches calls of these tools.
amountNoAn amount condition: currency plus at least one of gt, gte, lt, lte.
approversWith require_approval onlyWho may approve: roles and/or users. Required for require_approval, refused otherwise.
approval_ttl_secondsNo, require_approval onlyHow long the approval request stays open: an integer from 60 (1 minute) to 2592000 (30 days).
descriptionNoA one-line human reason, 1 to 300 characters. Shown in explanations and, at some levels, to agents. Language revision 2.
guardNo, require_approval onlyExactly true. Keeps the approval during observe windows that relax approvals. Language revision 2.
whenNo1 to 16 argument conditions, all of which must hold. Language revision 3.

How rules match a call

The Gateway knows four things about a call when it evaluates the policy: the tool's action (set when the tool was exposed, for example refund.issue), the exposed tool name (for example issue_refund), the call's amount, if the tool has an amount mapping, and the call's arguments, which only rules with argument conditions look at.

A rule is a candidate for a call when all of these hold:

  1. One of its actions patterns matches the tool's action.
  2. If it has tools, the exposed tool name is in the list.
  3. If it has an amount condition, the amount satisfies it (see Amount conditions).
  4. If it has a when list, its argument conditions hold (see Argument conditions).

Action patterns

PatternMatchesDoes not match
refund.issuerefund.issuerefund.issue.partial, refund
payouts.*payouts.send, payouts.batch.createpayouts
*every action—

A pattern is dot-separated segments of lowercase letters, digits, _ and -, with an optional final * segment, or * alone, at most 128 characters. Patterns in the reserved kblock. namespace are refused, because no tool action can use it.

Effects and the winner

Among the candidates, the most restrictive effect wins: deny beats require_approval, which beats allow. When several candidates have the winning effect, the first one in document order decides (so its approvers and TTL apply).

Rule order therefore matters only between rules with the same effect. A broad allow rule can never override a matching deny rule, wherever it is placed.

The default

When no rule is a candidate, the document's default decides:

  • "deny": the call is refused.
  • "require_approval": the call waits for approval by any member whose role is owner, admin or approver (the default has no approver list of its own).

A permissive default is not possible: anything you want to allow must be allowed by a rule.

Amount conditions and thresholds

An amount condition compares the call's amount with up to four thresholds, using exact decimal arithmetic (never floating point):

BoundMeaning
gtgreater than (exclusive)
gtegreater than or equal (inclusive)
ltless than (exclusive)
lteless than or equal (inclusive)

Write the thresholds as decimal strings, for example "100" or "99.50", with up to 18 digits before and 18 after the decimal point. A condition whose bounds admit no value (for example gt "10" and lt "5") is refused with CONTRADICTORY_BOUNDS.

The rule matches only when the call has an amount in the condition's currency that satisfies every bound you set. To cover a range without gaps, pair an exclusive bound with an inclusive one at the same threshold, as the example does with lte "100" and gt "100".

Missing amounts fail closed

When a call has no amount, or an amount in another currency than the condition's, K-Block cannot compare it. It then fails closed:

  • deny and require_approval rules with an amount condition match;
  • allow rules with an amount condition do not match.

In the example, a refund without a readable amount matches both refund-over-limit and refund-approval, and the deny wins. The decision's explanation records which rules matched only because of the missing amount. The validator's advisory warning FAIL_CLOSED_ROUTING tells you which tools are routed this way.

How amounts are read

A policy never reads tool arguments itself. The amount comes from the tool's amount mapping, which you set when you expose or edit the tool (see Expose and map tools):

Tool fieldMeaning
Amount pointer (optional)An RFC 6901 JSON Pointer into the call's arguments, for example /amount or /refund/value.
CurrencyThe ISO 4217 code the amounts are in, for example USD. Required with an amount pointer.
Currency pointer (optional)Where the call names its currency, if the upstream tool takes one, for example /currency.

Only a signed-in owner or admin can change the amount mapping of an enabled tool. The amount mapping is part of the tool's authorized binding: after any change, the Gateway does not run the tool's calls until an owner or admin authorizes the tool again.

Accepted amount values

The value at the amount pointer must be one of:

  • a non-negative decimal string: digits, optionally a . and 1 to 18 fraction digits, no sign, no exponent, no leading zeros ("12.50", "0.5", "1000");
  • a non-negative integer JSON number that is a safe integer (12).

There are no currency-specific decimal rules: "12.345" is accepted for USD. The string is used exactly as given.

Value at the pointerResult
"12.50", 12The call's amount is 12.50 or 12 in the tool's currency.
Nothing at the pointerThe call has no amount: amount rules fail closed.
12.5 (a non-integer number), "-1", "1e3", null, true, an objectThe call is denied with INVALID_AMOUNT.
An object on the path that also has a member differing only in letter case (amount and Amount)Denied with INVALID_AMOUNT, because an upstream could read the other member.

INVALID_AMOUNT is decided before the policy is evaluated, so no rule can allow such a call, and observe windows never relax it. An amount mapping that is itself invalid denies calls with INVALID_TOOL_CONFIGURATION.

Tools without an amount mapping

A tool without an amount mapping has no amounts: every call is treated as having no amount (it is priced 0 with the placeholder currency XXX in the signed task). Amount conditions in the policy never allow such calls, and a delegation with a spending limit refuses them.

Currencies

Three currencies can be involved: the tool's Currency, an optional currency the call names, and the currency of each rule's amount condition.

Fixed currency (no currency pointer)

Without a currency pointer, every amount of the tool is in the tool's currency. A currency argument the agent passes is not read: a call with {"amount": "5", "currency": "EUR"} on a USD tool is evaluated as 5 USD. Use this only when the upstream tool always works in that currency.

Currency pointer

With a currency pointer, the call must name exactly the tool's currency at that pointer. A different code, a lowercase code, or a missing value denies the call with INVALID_AMOUNT, and the upstream is never called. This prevents an upstream that honours the call's currency from executing an amount the policy priced in another currency.

Rule currency and tool currency differ

If a rule's amount condition names another currency than the tool's (for example the rule says EUR, the tool is mapped in USD), the comparison is impossible and the rule fails closed: deny and require_approval rules match, allow rules do not. Keep the currencies of your amount rules equal to the currencies of the tools they target.

Approvers in rules

A require_approval rule must say who may approve:

{ "roles": ["owner", "admin"], "users": ["<user-id>"] }
  • roles: one to three of owner, admin, approver.
  • users: 1 to 32 member user IDs (lowercase UUIDs). The Reference card in the editor lists the members who can approve, with their IDs.

A person is eligible when their current role is owner, admin or approver and either their role is listed or their user ID is listed. A listed user who later becomes a developer or viewer is no longer eligible. Validation refuses users who are not members (UNKNOWN_APPROVER) or whose role cannot approve (APPROVER_ROLE_NOT_CAPABLE).

When the rule decides a call, K-Block resolves the eligible people at that moment. Only members with an active K-Block signing identity count, because approving means signing. Two outcomes deny the call instead of creating a request:

CodeMeaning
NO_APPROVERSNobody eligible has an active identity.
TOO_MANY_APPROVERSMore than 32 eligible members: a K-Block task lists at most 32 approvers. Narrow the rule with fewer roles or explicit users.

Role-wide approvers count every member with that role and an active identity. The templates use only role approvers, and a require_approval default always admits every owner, admin and approver, so in an organization with more than 32 such members those calls are denied with TOO_MANY_APPROVERS until you list fewer roles or explicit users in the rule. A default cannot be narrowed: add a require_approval rule for the calls instead. Observe windows never relax either code.

The approval request stays open for the rule's approval_ttl_seconds, or the organization's default approval lifetime when the rule sets none, but never longer than the agent's delegation remains valid. See How approvals work.

Argument conditions

A rule can also decide on the call's arguments: the exact JSON object the agent (or your code, for a decision-only tool) sends, which the signed TASK binds. Add a when list to the rule: every condition in it must hold for the rule to apply.

conditions.json
{
  "schema": "kblock.cloud/policy@1",
  "default": "deny",
  "rules": [
    {
      "id": "non-prod-deploys",
      "actions": ["deploy.run"],
      "when": [{ "path": "/environment", "in": ["dev", "staging"] }],
      "effect": "allow"
    },
    {
      "id": "prod-deploys",
      "actions": ["deploy.run"],
      "when": [{ "path": "/environment", "eq": "production" }],
      "effect": "require_approval",
      "approvers": { "roles": ["owner", "admin"] }
    },
    {
      "id": "stock-adjustments",
      "actions": ["inventory.adjust"],
      "when": [
        { "path": "/lines/*/sku", "for": "all", "starts_with": ["SKU-"] },
        { "path": "/lines/*/qty", "for": "all", "gt": "0", "lte": "500" }
      ],
      "effect": "allow"
    }
  ]
}
CallDecisionDeciding rule
deploy.run with {"environment": "staging"}allownon-prod-deploys
deploy.run with {"environment": "production"}require approvalprod-deploys
deploy.run with {"environment": "qa"}denynone: the deny default
deploy.run without environmentrequire approvalprod-deploys, fail closed (see below)
inventory.adjust, every line an SKU- with a quantity from 1 to 500allowstock-adjustments
inventory.adjust, one line with quantity 900denynone: the deny default

Paths and wildcards

path is a JSON Pointer into the arguments object (RFC 6901): /environment, /customer/country, /lines/0/qty. A member name containing / or ~ is written ~1 and ~0. Array indexes are written in decimal without leading zeros.

A * token selects every element of an array: /lines/*/qty is the qty of each line. A condition whose path has a * needs "for": "all" (every selected element must satisfy it) or "for": "any" (at least one must); a path without * must not have for. Applied to an object instead of an array, * names the member "*", as JSON Pointer says.

Operators

Each condition has exactly one operator family:

OperatorOperandHolds when the value
eqa string, a whole number or a booleanis exactly equal (a string never equals a number; nothing is normalized: case, Unicode and spaces count)
in, not_in1 to 256 distinct values of those typesis one of them / none of them
gt, gte, lt, ltedecimal strings, combinable into a rangeis a whole number or a decimal string ("12.50", "-3") within the bounds, compared exactly, never as floating point
existstrue or falseis present / absent (null counts as present)
starts_with, ends_with1 to 64 non-empty stringsis a string with one of these prefixes / suffixes

There are no regular expressions, no arithmetic between arguments and no code: every condition is decided deterministically from the arguments alone.

Missing or malformed values fail closed

A condition can be true, false or indeterminate: the value it needs is missing, has another type (a number where a string is expected), is not a well-formed decimal, or a * path selects no element. K-Block never guesses:

  • an allow rule applies only when every condition is true, so a missing or malformed value can never gain an allow;
  • a deny or require_approval rule applies unless one condition is definitely false, so a missing value can never escape a deny or an approval.

In the example, a deploy without environment matches prod-deploys and waits for a person. for: "all" is false when any element is false, else indeterminate when any element is; for: "any" is true when any element is true, else indeterminate when any element is.

What agents and people see

Explanations name the rule and its conditions (path, operator and the policy's own operand), never the call's argument values. At the Rule and thresholds disclosure level, agents see the conditions of the deciding rule; see What agents are told. The action's page shows the arguments as signed in its evidence.

Language revision 3 and simulation

A document with any when list is language revision 3. Like revision 2, it can decide calls only once every Gateway supports it; until then its activation is refused with GATEWAYS_NOT_UPGRADED (see the language gate).

Policy tests and simulations evaluate argument conditions on the example's arguments (a JSON object of at most 8 KiB). A recorded call whose arguments are no longer available, because its operational data passed the retention period and it has no signed arguments, cannot be replayed against conditions: it is reported as not evaluable (ARGUMENTS_REQUIRED) instead of guessed. Decision maps and impact previews skip tools whose decision depends on arguments, and say so.

Descriptions and guards

Two optional rule members never change how a call is evaluated:

  • description is a human reason, shown in explanations to people and, at the disclosure levels Rule and approval details (rule, the default) and Rule and thresholds (conditions), to agents in refusals. Never put secrets in it. See What agents are told.
  • guard: true marks a require_approval rule that an observe window must never relax, even a window started to relax approvals. Without an observe window it has no effect.

Using either member makes the document language revision 2. Such a document can decide calls only once K-Block has enabled these newer policy features on every Gateway; until then, its activation is refused with GATEWAYS_NOT_UPGRADED. In that case, try again later, or contact K-Block support with the error code and the time of your attempt. See the language gate. Documents without them are revision 1 and are never held back.

Validation errors

Validation lists every problem with a JSON Pointer path. The most common codes:

CodeCause
UNKNOWN_MEMBER, MISSING_MEMBERA member is not allowed here, or a required one is missing.
INVALID_TYPEA value has the wrong JSON type, for example rules is not an array.
INVALID_SCHEMA, PERMISSIVE_DEFAULT, INVALID_DEFAULTWrong schema or default.
INVALID_RULE_ID, DUPLICATE_RULE_IDBad or repeated rule id.
INVALID_EFFECTeffect is not allow, require_approval or deny.
INVALID_ACTION_PATTERN, INVALID_TOOL_NAME, DUPLICATE_ENTRY, INVALID_LENGTHBad entries in actions or tools, or too many or too few.
INVALID_CURRENCY, INVALID_DECIMAL, MISSING_BOUND, CONTRADICTORY_BOUNDSBad amount condition.
APPROVERS_REQUIRED, APPROVERS_NOT_ALLOWED, APPROVERS_EMPTY, INVALID_ROLE, INVALID_USER_ID, UNKNOWN_APPROVER, APPROVER_ROLE_NOT_CAPABLEBad approvers.
INVALID_APPROVAL_TTL, APPROVAL_TTL_NOT_ALLOWEDBad or misplaced approval_ttl_seconds.
INVALID_DESCRIPTION, INVALID_GUARD, GUARD_NOT_ALLOWEDBad revision-2 members.
INVALID_CONDITION, INVALID_CONDITION_PATH, QUANTIFIER_REQUIRED, QUANTIFIER_NOT_ALLOWED, TOO_MANY_CONDITIONSBad argument condition: no or several operator families, a bad operand, a path that is not a JSON Pointer, a * path without for (or for without one), or more than 16 conditions in a rule or 512 in the document.

Templates and their parameters

A template is a ready-made document for a common kind of agent. Creating a policy from a template produces a draft built from your parameter values, plus saved example calls; it never activates anything (see Create a policy).

TemplateParameters (defaults)Recommended rollout
Support refunds (support-refunds@1)Refund currency (USD); Automatic refunds up to (100); Refunds with approval up to (1000)observe (test keys) → shadow → enforce
Coding and deployment (coding-deploy@1)noneshadow → enforce
CRM and sales (crm-sales@1)Discount currency (USD); Automatic discounts up to (500); Discounts need an admin above (5000)observe (test keys) → shadow → enforce
Finance operations (finance-ops@1)Payment currency (USD); Payments need the owner above (10000); Payments are denied above (50000)shadow → enforce (observe is not recommended for payments)
Read-only research (research-readonly@1)noneenforce

Parameter rules:

  • A currency parameter is an ISO 4217 code of three uppercase letters.
  • An amount parameter is a non-negative decimal string up to 1000000000000000 (10^15). It is normalized, so "100.00" and "100" produce the same document and digest.
  • Where a template has two amount limits, the lower must be strictly below the upper (for example the automatic refund limit below the approval limit), else the parameter is refused with PARAMETER_ORDER.
  • Unknown parameters are refused; omitted parameters take their defaults.

Every template document has a deny default, uses only role approvers (no user IDs), uses only revision-1 members, and lists the strictest amount rule for an action first. Each template also comes with example calls and their expected outcome, one-line notes per rule, and a delegation hint: the permissions (and, for amount templates, the maximum amount) to grant the agent's delegation, which stays the hard limit whatever the policy says.

Templates assume action names. Before you activate a draft from a template, compare its actions with your tools' actions and edit the document.

Next steps