Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 pointEventCheckVerdict
@0create po-1 for 1000not PayInvoicedeny
@10invoice po-1 for 10401040 <= 1000 + 50allow
@20invoice po-1 for 11001100 <= 1050 failsdeny
@30invoice po-2 for 40no po-2 yet, exists failsdeny
@40create po-2 for 200not PayInvoicedeny
@50invoice po-2 for 240240 <= 200 + 50allow

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 the po_amount + 50 tolerance.
  • schema.cedarschema: Procure action schema (CreatePurchaseOrder, PayInvoice, both with po_id/amount inputs).
  • 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]