touchless_invoice_within_tolerance
EXPERIMENTAL. This bundle uses arithmetic in temporal comparison operands, gated behind the off-by-default Cargo feature
experimental-arithmetic. A default build rejects this policy; the test harnesses check this bundle only when built with that feature.
This bundle exercises the + operator (tolerance comparisons).
A two-way invoice match with a tolerance: permit PayInvoice only when a
completed CreatePurchaseOrder (its response, not just a request that may
have failed) with the same po_id exists in the window and the invoice
amount is at most the PO amount plus 50.
permit (principal, action == Procure::Action::"PayInvoice", resource)
when temporal {
exists (po_amount: Long). (
formerly within 24h Procure::Action::"CreatePurchaseOrder"::response{
input.po_id: context.input.po_id,
input.amount: po_amount
}
&& context.input.amount <= po_amount + 50
)
};
Without arithmetic, two correlated event fields can only be compared exactly
(invoice <= po); the tolerance constant has nowhere to go.
Replay
| Decision point | Event | Check | Verdict |
|---|---|---|---|
@0 | create po-1 for 1000 | not PayInvoice | deny |
@10 | invoice po-1 for 1040 | 1040 <= 1000 + 50 | allow |
@20 | invoice po-1 for 1100 | 1100 <= 1050 fails | deny |
@30 | invoice po-2 for 40 | no po-2 yet, exists fails | deny |
@40 | create po-2 for 200 | not PayInvoice | deny |
@50 | invoice po-2 for 240 | 240 <= 200 + 50 | allow |
The history-only responses at @1 and @41 record that the two POs were
actually created; they yield no verdicts. Matching the response rather
than the request means a CreatePurchaseOrder call that never completed
would not enable payment.
@30 shows the tolerance never substitutes for the match: a small invoice
against a nonexistent PO is still denied.
Files
policy.dw: the permit with thepo_amount + 50tolerance.schema.cedarschema:Procureaction schema (CreatePurchaseOrder,PayInvoice, both withpo_id/amountinputs).trace.log: two POs (each a request plus its response) and four invoices by alice.expected.out: the per-decision-point verdict stream.
Reproduce (run from this directory):
dogwood validate policy.dw --policy-schema schema.cedarschema
dogwood replay policy.dw --policy-schema schema.cedarschema --trace trace.log
World: procure. Default event schema; no providers.
Referenced by guide/04-temporal-expressions.md (arithmetic in comparison operands).
Policy
// Two-way invoice match with a tolerance: an invoice is auto-payable only
// when a completed purchase-order creation (a `CreatePurchaseOrder`
// response, not just a request that may have failed) with the same id
// exists in the look-back window and the invoice amount is at most the PO
// amount plus 50.
//
// The `po_amount + 50` term is the arithmetic this bundle exists to
// exercise: without it, two correlated event fields can only be compared
// exactly (`invoice <= po`), and a tolerance constant has nowhere to go.
//
@id("touchless_invoice_within_tolerance")
permit (
principal,
action == Procure::Action::"PayInvoice",
resource
)
when temporal {
exists (po_amount: Long). (
formerly within 24h Procure::Action::"CreatePurchaseOrder"::response{
input.po_id: context.input.po_id,
input.amount: po_amount
}
&& context.input.amount <= po_amount + 50
)
};
Schema
namespace Procure {
type CreatePurchaseOrderInput = {
po_id: String,
amount: Long
};
type CreatePurchaseOrderOutput = { };
type PayInvoiceInput = {
po_id: String,
amount: Long
};
type PayInvoiceOutput = { };
type SystemContext = {
now: datetime
};
entity Gateway;
entity OAuthUser = {
id: String
} tags String;
action "CreatePurchaseOrder" appliesTo {
principal: [OAuthUser],
resource: [Gateway],
context: {
input: CreatePurchaseOrderInput,
output?: CreatePurchaseOrderOutput,
system: SystemContext
}
};
action "PayInvoice" appliesTo {
principal: [OAuthUser],
resource: [Gateway],
context: {
input: PayInvoiceInput,
output?: PayInvoiceOutput,
system: SystemContext
}
};
}
Trace
@0 scope(principal: Procure::OAuthUser::"alice", resource: Procure::Gateway::"gw1") request_context(input: { po_id: "po-1", amount: 1000 }) Procure::Action::"CreatePurchaseOrder"::request(input: { po_id: "po-1", amount: 1000 }, callerPrincipal: Procure::OAuthUser::"alice", callerResource: Procure::Gateway::"gw1", requestId: "p1")
@1 scope(principal: Procure::OAuthUser::"alice", resource: Procure::Gateway::"gw1") Procure::Action::"CreatePurchaseOrder"::response(input: { po_id: "po-1", amount: 1000 }, callerPrincipal: Procure::OAuthUser::"alice", callerResource: Procure::Gateway::"gw1", requestId: "p1")
@10 scope(principal: Procure::OAuthUser::"alice", resource: Procure::Gateway::"gw1") request_context(input: { po_id: "po-1", amount: 1040 }) Procure::Action::"PayInvoice"::request(input: { po_id: "po-1", amount: 1040 }, callerPrincipal: Procure::OAuthUser::"alice", callerResource: Procure::Gateway::"gw1", requestId: "i1")
@20 scope(principal: Procure::OAuthUser::"alice", resource: Procure::Gateway::"gw1") request_context(input: { po_id: "po-1", amount: 1100 }) Procure::Action::"PayInvoice"::request(input: { po_id: "po-1", amount: 1100 }, callerPrincipal: Procure::OAuthUser::"alice", callerResource: Procure::Gateway::"gw1", requestId: "i2")
@30 scope(principal: Procure::OAuthUser::"alice", resource: Procure::Gateway::"gw1") request_context(input: { po_id: "po-2", amount: 40 }) Procure::Action::"PayInvoice"::request(input: { po_id: "po-2", amount: 40 }, callerPrincipal: Procure::OAuthUser::"alice", callerResource: Procure::Gateway::"gw1", requestId: "i3")
@40 scope(principal: Procure::OAuthUser::"alice", resource: Procure::Gateway::"gw1") request_context(input: { po_id: "po-2", amount: 200 }) Procure::Action::"CreatePurchaseOrder"::request(input: { po_id: "po-2", amount: 200 }, callerPrincipal: Procure::OAuthUser::"alice", callerResource: Procure::Gateway::"gw1", requestId: "p2")
@41 scope(principal: Procure::OAuthUser::"alice", resource: Procure::Gateway::"gw1") Procure::Action::"CreatePurchaseOrder"::response(input: { po_id: "po-2", amount: 200 }, callerPrincipal: Procure::OAuthUser::"alice", callerResource: Procure::Gateway::"gw1", requestId: "p2")
@50 scope(principal: Procure::OAuthUser::"alice", resource: Procure::Gateway::"gw1") request_context(input: { po_id: "po-2", amount: 240 }) Procure::Action::"PayInvoice"::request(input: { po_id: "po-2", amount: 240 }, callerPrincipal: Procure::OAuthUser::"alice", callerResource: Procure::Gateway::"gw1", requestId: "i4")
Expected Output
@0 (time point 0): DENY
@10 (time point 1): ALLOW [rules: 0]
@20 (time point 2): DENY
@30 (time point 3): DENY
@40 (time point 4): DENY
@50 (time point 5): ALLOW [rules: 0]