Language
Patterns
v1 leaves out negation, arithmetic, functions and lists, so that every decision is a short, predictable sequence of comparisons over facts. These patterns cover what people usually reach for those features to do. Each one is checked by ulang-lite.
Saying "not"
NOT is refused before parsingpat-not-badCONTRACT Publish {
ENFORCEMENT_MODE: enforced
CONTEXT {
ENTITIES Article {
reviewed: bool
word_count: int
status: string
}
}
TRIGGER {
publish()
}
PROHIBITION {
FORBID publish WHEN NOT Article.reviewed == true [SOURCE:needs_review]
}
}
rejected unsupported[not-operator] at 14:25 NOT is refused before recursive parsing
Write the opposite comparison instead:
| Instead of | Write |
|---|---|
not a == b | a != b |
not a < b | a >= b |
not a <= b | a > b |
not flag == true | flag == false |
not (A AND B) | (not A) OR (not B) |
not (A OR B) | (not A) AND (not B) |
CONTRACT Publish {
ENFORCEMENT_MODE: enforced
CONTEXT {
ENTITIES Article {
reviewed: bool
word_count: int
status: string
}
}
TRIGGER {
publish()
}
PROHIBITION {
FORBID publish WHEN Article.reviewed == false [SOURCE:needs_review]
FORBID publish WHEN Article.word_count < 300 [SOURCE:too_short]
FORBID publish WHEN Article.status != "ready" AND Article.status != "scheduled" [SOURCE:wrong_status]
}
}
valid Publish 3 clauses · sha256 e21f874658d8a743229e186939fa8b3baf72548147242642ca341549fc6c8987
Facts of the first request; each other request changes only the fields shown.
{
"Article": {
"reviewed": true,
"word_count": 800,
"status": "ready"
}
}
| Request | Action | Changed | Verdict | Clause |
|---|---|---|---|---|
| reviewed, 800 words, ready | publish | as above | PERMIT | Publish.publish.permit |
| not reviewed | publish | reviewed: false | DENY | Publish.needs_review |
| a draft | publish | status: "draft" | DENY | Publish.wrong_status |
The three letters NOT are refused anywhere outside strings, comments and source tags, even inside a name such as CANNOT_MERGE. Use lower-case names.
Allow-lists
A chain of != joined with AND denies everything except the listed values:
CONTRACT Shipping {
ENFORCEMENT_MODE: enforced
CONTEXT {
ENTITIES Shipment {
country: string
weight_kg: float
hazardous: bool
}
CONSTANTS {
max_weight_kg: float == 30.0 [SOURCE:CARRIER-TERMS-2]
}
}
TRIGGER {
ship()
}
PROHIBITION {
FORBID ship WHEN Shipment.country != "US" AND Shipment.country != "CA" AND Shipment.country != "MX" [SOURCE:ship_regions]
FORBID ship WHEN Shipment.hazardous == true OR Shipment.weight_kg > max_weight_kg [SOURCE:carrier_limits]
}
}
valid Shipping 2 clauses · sha256 849104d027b84a94dfdbc5ee0739dd33fa5c7f66143d5761e05f8ee9e07132f4
Facts of the first request; each other request changes only the fields shown.
{
"Shipment": {
"country": "US",
"weight_kg": 2,
"hazardous": false
}
}
| Request | Action | Changed | Verdict | Clause |
|---|---|---|---|---|
| US, 2 kg | ship | as above | PERMIT | Shipping.ship.permit |
| France | ship | country: "FR" | DENY | Shipping.ship_regions |
| "us" in lower case | ship | country: "us" | DENY | Shipping.ship_regions |
| Canada, 31 kg | ship | country: "CA" weight_kg: 31 | DENY | Shipping.carrier_limits |
Matching is exact. If case should not matter, normalise it in the application.
Ranges
CONTRACT Setpoint {
ENFORCEMENT_MODE: enforced
CONTEXT {
ENTITIES Change {
celsius: float
operator_certified: bool
}
CONSTANTS {
min_c: float == 5.0 [SOURCE:SAFETY-1]
max_c: float == 28.0 [SOURCE:SAFETY-1]
}
}
TRIGGER {
set_temperature()
}
GUARANTEE {
ENSURE set_temperature() WHEN Change.celsius >= min_c AND Change.celsius <= max_c [SOURCE:in_range]
}
PROHIBITION {
FORBID set_temperature WHEN Change.celsius > 24.0 AND Change.operator_certified == false [SOURCE:certified_above_24]
}
}
valid Setpoint 2 clauses · sha256 a55e62765e7467b7cd7309c8219c9af2f7dd80d6ee9a7e6b53a9958ab2c75cc4
Facts of the first request; each other request changes only the fields shown.
{
"Change": {
"celsius": 21,
"operator_certified": false
}
}
| Request | Action | Changed | Verdict | Clause |
|---|---|---|---|---|
| 21 °C | set_temperature | as above | PERMIT | Setpoint.set_temperature.permit |
| 3 °C | set_temperature | celsius: 3 | DENY | Setpoint.in_range |
| 26 °C, not certified | set_temperature | celsius: 26 | DENY | Setpoint.certified_above_24 |
| 26 °C, certified | set_temperature | celsius: 26 operator_certified: true | PERMIT | Setpoint.set_temperature.permit |
ENSURE x >= min AND x <= max names one clause for the whole range. Separate FORBID clauses for each side name the side that was crossed.
Comparing two facts
Two numeric facts can be compared directly. Use integer minor units, such as cents, for money, so that equality is exact.
CONTRACT Purchasing {
ENFORCEMENT_MODE: enforced
CONTEXT {
ENTITIES Purchase {
amount_cents: int
remaining_budget_cents: int
vendor_approved: bool
}
}
TRIGGER {
purchase()
}
GUARANTEE {
ENSURE purchase() WHEN Purchase.vendor_approved == true [SOURCE:approved_vendor]
}
PROHIBITION {
FORBID purchase WHEN Purchase.amount_cents > Purchase.remaining_budget_cents [SOURCE:over_budget]
}
}
valid Purchasing 2 clauses · sha256 80232a79bb4efcf53f35bbfb38f7695e895ebc466f44c79190a4c0c6576a6bd2
Facts of the first request; each other request changes only the fields shown.
{
"Purchase": {
"amount_cents": 5000,
"remaining_budget_cents": 20000,
"vendor_approved": true
}
}
| Request | Action | Changed | Verdict | Clause |
|---|---|---|---|---|
| $50 of $200 left | purchase | as above | PERMIT | Purchasing.purchase.permit |
| $250 of $200 left | purchase | amount_cents: 25000 | DENY | Purchasing.over_budget |
Computed values
CONTRACT Quota {
ENFORCEMENT_MODE: enforced
CONTEXT {
ENTITIES Usage {
used: int
requested: int
limit: int
}
}
TRIGGER {
reserve()
}
PROHIBITION {
FORBID reserve WHEN Usage.used + Usage.requested > Usage.limit [SOURCE:quota]
}
}
rejected unsupported[arithmetic] at 14:36 arithmetic is refused before recursive parsing
Contracts compare; applications compute. Pass the computed value as a fact, named for what it means:
CONTRACT Quota {
ENFORCEMENT_MODE: enforced
CONTEXT {
ENTITIES Usage {
used: int
requested: int
projected: int
limit: int
}
}
TRIGGER {
reserve()
}
PROHIBITION {
FORBID reserve WHEN Usage.projected > Usage.limit [SOURCE:quota]
}
}
valid Quota 1 clause · sha256 04b0f1188e1e0b8f95d4e1039faa609a6e8df7b9afb4751d95d7f8071f4d3427
Facts of the first request; each other request changes only the fields shown.
{
"Usage": {
"used": 70,
"requested": 20,
"projected": 90,
"limit": 100
}
}
| Request | Action | Changed | Verdict | Clause |
|---|---|---|---|---|
| 70 used + 20 requested | reserve | as above | PERMIT | Quota.reserve.permit |
| 70 used + 40 requested | reserve | requested: 40 projected: 110 | DENY | Quota.quota |
Keep computed facts few and obvious, and compute each in one place: a contract can only be as right as the facts it is given.
Relations between facts
Two string facts cannot be compared with each other:
CONTRACT Approvals {
ENFORCEMENT_MODE: enforced
CONTEXT {
ENTITIES Approval {
approver_id: string
requester_id: string
approver_is_requester: bool
}
}
TRIGGER {
approve()
}
PROHIBITION {
FORBID approve WHEN Approval.approver_id == Approval.requester_id [SOURCE:four_eyes]
}
}
rejected unsupported[nonnumeric-comparison] at 14:25 nonnumeric comparisons require a field and literal
Compute the relation in the application and pass it as a boolean:
CONTRACT Approvals {
ENFORCEMENT_MODE: enforced
CONTEXT {
ENTITIES Approval {
approver_id: string
requester_id: string
approver_is_requester: bool
}
}
TRIGGER {
approve()
}
PROHIBITION {
FORBID approve WHEN Approval.approver_is_requester == true [SOURCE:four_eyes]
}
}
valid Approvals 1 clause · sha256 f6a9fe155f7924641bbe29b6427d5bd4681ae4656de70524db30ca0343940905
Facts of the first request; each other request changes only the fields shown.
{
"Approval": {
"approver_id": "u-17",
"requester_id": "u-42",
"approver_is_requester": false
}
}
| Request | Action | Changed | Verdict | Clause |
|---|---|---|---|---|
| someone else approves | approve | as above | PERMIT | Approvals.approve.permit |
| the requester approves | approve | approver_id: "u-42" approver_is_requester: true | DENY | Approvals.four_eyes |
Time
A contract has no clock. The application supplies time as facts, in whatever form the rule needs:
CONTRACT ChangeWindow {
ENFORCEMENT_MODE: enforced
CONTEXT {
ENTITIES Window {
weekday: int
hour_utc: int
}
}
TRIGGER {
deploy()
}
PROHIBITION {
FORBID deploy WHEN Window.weekday >= 5 OR Window.hour_utc < 9 OR Window.hour_utc >= 17 [SOURCE:business_hours]
}
}
valid ChangeWindow 1 clause · sha256 d218eaff9511e87499bbb84b769fb22e58eeca02d08cbe750b785fe7194aa438
Facts of the first request; each other request changes only the fields shown.
{
"Window": {
"weekday": 1,
"hour_utc": 14
}
}
| Request | Action | Changed | Verdict | Clause |
|---|---|---|---|---|
| Tuesday 14:00 | deploy | as above | PERMIT | ChangeWindow.deploy.permit |
| Saturday 14:00 | deploy | weekday: 5 | DENY | ChangeWindow.business_hours |
| Tuesday 18:00 | deploy | hour_utc: 18 | DENY | ChangeWindow.business_hours |
Here weekday is 0 for Monday, by the application's convention. Write such conventions down next to the code that builds the facts.
Ordering prohibitions
When several prohibitions apply, the first in source order is named. These two contracts have the same rules in a different order:
CONTRACT Payouts {
ENFORCEMENT_MODE: enforced
CONTEXT {
ENTITIES Account {
frozen: bool
amount_cents: int
balance_cents: int
}
}
TRIGGER {
withdraw()
}
PROHIBITION {
FORBID withdraw WHEN Account.frozen == true [SOURCE:account_frozen]
FORBID withdraw WHEN Account.amount_cents > Account.balance_cents [SOURCE:insufficient_funds]
}
}
valid Payouts 2 clauses · sha256 7425e5b3fb960f154ecacc95d45edcd68073f269bb9180f80b14cbf5a2fbdf18
Facts:
{
"Account": {
"frozen": true,
"amount_cents": 9000,
"balance_cents": 5000
}
}
| Request | Action | Verdict | Clause |
|---|---|---|---|
| frozen and overdrawn | withdraw | DENY | Payouts.account_frozen |
CONTRACT Payouts {
ENFORCEMENT_MODE: enforced
CONTEXT {
ENTITIES Account {
frozen: bool
amount_cents: int
balance_cents: int
}
}
TRIGGER {
withdraw()
}
PROHIBITION {
FORBID withdraw WHEN Account.amount_cents > Account.balance_cents [SOURCE:insufficient_funds]
FORBID withdraw WHEN Account.frozen == true [SOURCE:account_frozen]
}
}
valid Payouts 2 clauses · sha256 9ae8ce9470978d9eac4216cec9c931cbe47d616e382cff79b9f84326d254e1b0
Facts:
{
"Account": {
"frozen": true,
"amount_cents": 9000,
"balance_cents": 5000
}
}
| Request | Action | Verdict | Clause |
|---|---|---|---|
| frozen and overdrawn | withdraw | DENY | Payouts.insufficient_funds |
Both deny. For a frozen account, only the first gives the reason that matters. Order prohibitions from the most useful explanation to the least.