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

macro_library_once_is_small

Demonstrates the shareable macro library: the policy calls once (a def temporal macro) and is_small (a def cedar macro) that are defined in an external macros.dw and supplied via --macros. The policy.dw does not redeclare either macro — they are merged in from the library at lowering time.

permit SellShares only when the sale amount is_small(context.input.shares) (the Cedar macro, < 100) and the same stock had an ApproveSale once within the last hour (the temporal macro, formerly within 1h, pinned to context.input.stock).

Run it (from this directory):

dogwood validate policy.dw --policy-schema schema.cedarschema --macros macros.dw
dogwood replay   policy.dw --policy-schema schema.cedarschema --macros macros.dw --trace trace.log

The trace shows every case:

  • @0ApproveSale for AMZN by alice (history-only; no SellShares permit applies, so the decision is a deny).
  • @100 — alice sells 5 AMZN, 100s after the approval → allow (is_small(5) holds and a matching approval is within the window).
  • @200 — alice sells 500 AMZN → deny (is_small(500) is false: the Cedar macro’s < 100 threshold fails).
  • @5000 — bob sells MSFT with no prior approval → deny (once finds no matching ApproveSale in the window).

This is the only chapter example that needs --macros.

Referenced by guide/06-macros.md.

Policy

// This policy calls `is_small` (a Cedar macro) and `once` (a temporal
// condition macro) that are DEFINED in the attached macro LIBRARY
// (macros.dw, supplied via --macros), not redeclared here.
@id("sell_small_recent_approval")
permit (
    principal,
    action == Drupe::Action::"SellShares",
    resource
)
when {
    is_small(context.input.shares)
    && temporal {
        once(1h, Drupe::Action::"ApproveSale"::request{
            input.stock: context.input.stock
        })
    }
};

Schema

namespace Drupe {
  type ApproveSaleInput = {
    shares: Long,
    stock: String
  };

  type ApproveSaleOutput = {
    approved: Bool
  };

  type ContentFilterFinding = {
    severityScore: decimal
  };

  type GetStockInfoInput = {
    stock: String
  };

  type GetStockInfoOutput = {
    info: String
  };

  type PromptAttackFinding = {
    severityScore: decimal
  };

  type SellSharesInput = {
    shares: Long,
    stock: String
  };

  type SellSharesOutput = {
    proceeds: decimal
  };

  type SensitiveInfoFinding = {
    confidenceScore: decimal
  };

  type SystemContext = {
    now: datetime
  };

  entity Gateway;

  entity IamEntity = {
    id: String
  };

  entity OAuthUser = {
    id: String
  } tags String;

  entity UnauthenticatedUser;

  action "ApproveSale" in [Action::"CallTool"] appliesTo {
    principal: [IamEntity, OAuthUser, UnauthenticatedUser],
    resource: [Gateway],
    context: {
      input: ApproveSaleInput,
      output?: ApproveSaleOutput,
      system: SystemContext
    }
  };

  action "CallTool" in [Action::"Mcp"] appliesTo {
    principal: [OAuthUser, IamEntity, UnauthenticatedUser],
    resource: [Gateway],
    context: {
      system: SystemContext
    }
  };

  action "GetStockInfo" in [Action::"CallTool"] appliesTo {
    principal: [IamEntity, OAuthUser, UnauthenticatedUser],
    resource: [Gateway],
    context: {
      input: GetStockInfoInput,
      output?: GetStockInfoOutput,
      system: SystemContext
    }
  };

  action "Http" appliesTo {
    principal: [OAuthUser, IamEntity, UnauthenticatedUser],
    resource: [Gateway],
    context: {
      system: SystemContext
    }
  };

  action "InvokeAgent" in [Action::"Http"] appliesTo {
    principal: [OAuthUser, IamEntity, UnauthenticatedUser],
    resource: [Gateway],
    context: {
      input?: {      },
      system: SystemContext
    }
  };

  action "InvokeLLM" in [Action::"Http"] appliesTo {
    principal: [OAuthUser, IamEntity, UnauthenticatedUser],
    resource: [Gateway],
    context: {
      input?: {      },
      system: SystemContext
    }
  };

  action "Mcp" appliesTo {
    principal: [OAuthUser, IamEntity, UnauthenticatedUser],
    resource: [Gateway],
    context: {
      system: SystemContext
    }
  };

  action "SellShares" in [Action::"CallTool"] appliesTo {
    principal: [IamEntity, OAuthUser, UnauthenticatedUser],
    resource: [Gateway],
    context: {
      input: SellSharesInput,
      output?: SellSharesOutput,
      system: SystemContext
    }
  };

  action "UnknownTool" in [Action::"CallTool"] appliesTo {
    principal: [OAuthUser, IamEntity, UnauthenticatedUser],
    resource: [Gateway],
    context: {
      system: SystemContext
    }
  };
}

Trace

@0 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { shares: 5, stock: "AMZN" }) Drupe::Action::"ApproveSale"::request(input: { shares: 5, stock: "AMZN" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u1")
@100 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { shares: 5, stock: "AMZN" }) Drupe::Action::"SellShares"::request(input: { shares: 5, stock: "AMZN" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u2")
@200 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { shares: 500, stock: "AMZN" }) Drupe::Action::"SellShares"::request(input: { shares: 500, stock: "AMZN" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u3")
@5000 scope(principal: Drupe::OAuthUser::"bob", resource: Drupe::Gateway::"gw1") request_context(input: { shares: 5, stock: "MSFT" }) Drupe::Action::"SellShares"::request(input: { shares: 5, stock: "MSFT" }, callerPrincipal: Drupe::OAuthUser::"bob", callerResource: Drupe::Gateway::"gw1", requestId: "u4")

Expected Output

@0 (time point 0): DENY
@100 (time point 1): ALLOW  [rules: 0]
@200 (time point 2): DENY
@5000 (time point 3): DENY

Macros

// Shareable macro library, attached to a schema via --macros. These
// `def` definitions are merged into every policy set lowered against the
// schema, so policies can call `once` and `is_small` without redeclaring
// them.
def temporal once(?w, ?s) { formerly within ?w ?s };
def cedar    is_small(?n) { ?n < 100 };