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:
@0—ApproveSalefor AMZN by alice (history-only; noSellSharespermit 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< 100threshold fails).@5000— bob sells MSFT with no prior approval → deny (oncefinds no matchingApproveSalein 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 };