temporal_sum_formerly_transfer
A temporal sum aggregation macro defined inline. sum_formerly combines a
binder-position parameter ?a (the sum’s bound variable, passed by the
caller as the bare identifier a) with a macro-introduced hygienic binder
$t. It desugars to:
sum ?a for (?a: Long), ($t: Timepoint). where (formerly within ?w (?body && tp($t)))
The policy sums every Transfer’s input.amount seen within the last hour and
permits Alert only when that running total exceeds 100, comparing the
aggregate inside an exists (total: Long) binder.
The trace shows both outcomes:
@0—Transferof 40 by alice (history-only; running sum = 40).@2—Alert: cumulative Transfer sum within 1h is 40, not> 100→ DENY.@4—Transferof 80 by bob (running sum = 40 + 80 = 120).@6—Alert: cumulative Transfer sum is 120, which is> 100→ ALLOW.
Note: the guide uses total > 100; the corpus source policy
(tests/passing/macros/corpus/0010_sum_formerly_exact) uses total == 100.
This example follows the guide (> 100), so expected.out was captured from a
fresh dogwood replay of this policy.
Referenced by guide/06-macros.md.
Policy
// `sum_formerly` uses a binder-position parameter `?a` (the sum's bound
// variable, passed as the bare identifier `a`) plus a macro-introduced fresh
// binder `$t`. The aggregate is compared inside an `exists (total: Long)`.
def temporal sum_formerly(?a, ?w, ?body) {
sum ?a for (?a: Long), ($t: Timepoint). where (formerly within ?w (?body && tp($t)))
};
@id("transfer_sum_over_100")
permit (
principal,
action == Drupe::Action::"Alert",
resource
)
when temporal {
exists (total: Long). (
(sum_formerly(a, 1h, Drupe::Action::"Transfer"::request{
input.user: _, input.amount: a
})) == total
&& total > 100
)
};
Schema
namespace Drupe {
type AlertInput = {
level: Long,
server: String
};
type AlertOutput = { };
type ContentFilterFinding = {
severityScore: decimal
};
type HeartbeatInput = {
server: String
};
type HeartbeatOutput = { };
type LoginInput = {
server: String,
user: String
};
type LoginOutput = { };
type PromptAttackFinding = {
severityScore: decimal
};
type SensitiveInfoFinding = {
confidenceScore: decimal
};
type SystemContext = {
now: datetime
};
type TransferInput = {
amount: Long,
user: String
};
type TransferOutput = { };
entity Gateway;
entity IamEntity = {
id: String
};
entity OAuthUser = {
id: String
} tags String;
entity UnauthenticatedUser;
action "Alert" in [Action::"CallTool"] appliesTo {
principal: [IamEntity, OAuthUser, UnauthenticatedUser],
resource: [Gateway],
context: {
input: AlertInput,
output?: AlertOutput,
system: SystemContext
}
};
action "CallTool" in [Action::"Mcp"] appliesTo {
principal: [OAuthUser, IamEntity, UnauthenticatedUser],
resource: [Gateway],
context: {
system: SystemContext
}
};
action "Heartbeat" in [Action::"CallTool"] appliesTo {
principal: [IamEntity, OAuthUser, UnauthenticatedUser],
resource: [Gateway],
context: {
input: HeartbeatInput,
output?: HeartbeatOutput,
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 "Login" in [Action::"CallTool"] appliesTo {
principal: [IamEntity, OAuthUser, UnauthenticatedUser],
resource: [Gateway],
context: {
input: LoginInput,
output?: LoginOutput,
system: SystemContext
}
};
action "Mcp" appliesTo {
principal: [OAuthUser, IamEntity, UnauthenticatedUser],
resource: [Gateway],
context: {
system: SystemContext
}
};
action "Transfer" in [Action::"CallTool"] appliesTo {
principal: [IamEntity, OAuthUser, UnauthenticatedUser],
resource: [Gateway],
context: {
input: TransferInput,
output?: TransferOutput,
system: SystemContext
}
};
action "UnknownTool" in [Action::"CallTool"] appliesTo {
principal: [OAuthUser, IamEntity, UnauthenticatedUser],
resource: [Gateway],
context: {
system: SystemContext
}
};
}
Trace
@0 scope(principal: Drupe::OAuthUser::"s1", resource: Drupe::Gateway::"gw1") request_context(input: { amount: 40, user: "alice" }) Drupe::Action::"Transfer"::request(input: { amount: 40, user: "alice" }, callerPrincipal: Drupe::OAuthUser::"s1", callerResource: Drupe::Gateway::"gw1", requestId: "u1")
@1 scope(principal: Drupe::OAuthUser::"s1", resource: Drupe::Gateway::"gw1") Drupe::Action::"Transfer"::response(input: { amount: 40, user: "alice" }, callerPrincipal: Drupe::OAuthUser::"s1", callerResource: Drupe::Gateway::"gw1", requestId: "u1")
@2 scope(principal: Drupe::OAuthUser::"s1", resource: Drupe::Gateway::"gw1") request_context(input: { level: 1, server: "s1" }) Drupe::Action::"Alert"::request(input: { level: 1, server: "s1" }, callerPrincipal: Drupe::OAuthUser::"s1", callerResource: Drupe::Gateway::"gw1", requestId: "a1")
@3 scope(principal: Drupe::OAuthUser::"s1", resource: Drupe::Gateway::"gw1") Drupe::Action::"Alert"::response(input: { level: 1, server: "s1" }, callerPrincipal: Drupe::OAuthUser::"s1", callerResource: Drupe::Gateway::"gw1", requestId: "a1")
@4 scope(principal: Drupe::OAuthUser::"s1", resource: Drupe::Gateway::"gw1") request_context(input: { amount: 80, user: "bob" }) Drupe::Action::"Transfer"::request(input: { amount: 80, user: "bob" }, callerPrincipal: Drupe::OAuthUser::"s1", callerResource: Drupe::Gateway::"gw1", requestId: "u2")
@5 scope(principal: Drupe::OAuthUser::"s1", resource: Drupe::Gateway::"gw1") Drupe::Action::"Transfer"::response(input: { amount: 80, user: "bob" }, callerPrincipal: Drupe::OAuthUser::"s1", callerResource: Drupe::Gateway::"gw1", requestId: "u2")
@6 scope(principal: Drupe::OAuthUser::"s1", resource: Drupe::Gateway::"gw1") request_context(input: { level: 2, server: "s1" }) Drupe::Action::"Alert"::request(input: { level: 2, server: "s1" }, callerPrincipal: Drupe::OAuthUser::"s1", callerResource: Drupe::Gateway::"gw1", requestId: "a2")
@7 scope(principal: Drupe::OAuthUser::"s1", resource: Drupe::Gateway::"gw1") Drupe::Action::"Alert"::response(input: { level: 2, server: "s1" }, callerPrincipal: Drupe::OAuthUser::"s1", callerResource: Drupe::Gateway::"gw1", requestId: "a2")
Expected Output
@0 (time point 0): DENY
@2 (time point 1): DENY
@4 (time point 2): DENY
@6 (time point 3): ALLOW [rules: 0]