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

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:

  • @0Transfer of 40 by alice (history-only; running sum = 40).
  • @2Alert: cumulative Transfer sum within 1h is 40, not > 100DENY.
  • @4Transfer of 80 by bob (running sum = 40 + 80 = 120).
  • @6Alert: cumulative Transfer sum is 120, which is > 100ALLOW.

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]