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

alert_total_transfer_over_200

Aggregation example: permit an Alert only when the total transferred amount exceeds a threshold. sum a for (a: Long). where …Transfer::request{ input.amount: a } sums the amount column over the deduplicated matching rows, binds it to total via == total, and the permit fires only if total > 200.

The sum body is a bare predicate — it has no formerly/temporal wrapper, so it scans only the current timepoint. Because the permit’s scope requires the current action to be Alert (never a Transfer), the summed relation at every Alert decision point is empty, so total is 0 and total > 200 is never satisfied. Every verdict in the trace is therefore a deny — matching the all-false oracle for corpus 0063_sum_threshold. (To make this fire you would wrap the body in a temporal operator so the sum ranges over past transfers, as in the 0299_sum_resolved_filter / 0301_sum_resolved_range_filter variants.)

Files:

  • policy.dw — the permit with the sum … > 200 temporal body.
  • schema.cedarschema — Drupe action schema (lifted from corpus temporal_only/0063_sum_threshold; has Transfer with a Long amount input and Alert).
  • trace.log — lifted from 0063_sum_threshold/trace_1.log: transfers of 100, 200, and 50 by three users interleaved with three Alerts.
  • expected.out — the real per-timepoint verdict stream from dogwood replay.

Reproduce (run from this directory):

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

Referenced by guide/04-temporal-expressions.md.

Policy

// `sum v` sums column v over the deduplicated matching rows; v must be a
// for-declared variable. Permit an Alert only if the total transferred amount
// exceeds 200.
@id("alert_total_transfer_over_200")
permit (
    principal,
    action == Drupe::Action::"Alert",
    resource
)
when temporal {
    exists (total: Long). (
        (sum a for (a: Long). where Drupe::Action::"Transfer"::request{ input.amount: a }) == total
        && total > 200
    )
};

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::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { amount: 100, user: "alice" }) Drupe::Action::"Transfer"::request(input: { amount: 100, user: "alice" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u1")
@1 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") Drupe::Action::"Transfer"::response(input: { amount: 100, user: "alice" }, callerPrincipal: Drupe::OAuthUser::"alice", 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: "u2")
@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: "u2")
@10 scope(principal: Drupe::OAuthUser::"bob", resource: Drupe::Gateway::"gw1") request_context(input: { amount: 200, user: "bob" }) Drupe::Action::"Transfer"::request(input: { amount: 200, user: "bob" }, callerPrincipal: Drupe::OAuthUser::"bob", callerResource: Drupe::Gateway::"gw1", requestId: "u3")
@11 scope(principal: Drupe::OAuthUser::"bob", resource: Drupe::Gateway::"gw1") Drupe::Action::"Transfer"::response(input: { amount: 200, user: "bob" }, callerPrincipal: Drupe::OAuthUser::"bob", callerResource: Drupe::Gateway::"gw1", requestId: "u3")
@12 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: "u4")
@13 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: "u4")
@20 scope(principal: Drupe::OAuthUser::"carol", resource: Drupe::Gateway::"gw1") request_context(input: { amount: 50, user: "carol" }) Drupe::Action::"Transfer"::request(input: { amount: 50, user: "carol" }, callerPrincipal: Drupe::OAuthUser::"carol", callerResource: Drupe::Gateway::"gw1", requestId: "u5")
@21 scope(principal: Drupe::OAuthUser::"carol", resource: Drupe::Gateway::"gw1") Drupe::Action::"Transfer"::response(input: { amount: 50, user: "carol" }, callerPrincipal: Drupe::OAuthUser::"carol", callerResource: Drupe::Gateway::"gw1", requestId: "u5")
@22 scope(principal: Drupe::OAuthUser::"s1", resource: Drupe::Gateway::"gw1") request_context(input: { level: 3, server: "s1" }) Drupe::Action::"Alert"::request(input: { level: 3, server: "s1" }, callerPrincipal: Drupe::OAuthUser::"s1", callerResource: Drupe::Gateway::"gw1", requestId: "u6")
@23 scope(principal: Drupe::OAuthUser::"s1", resource: Drupe::Gateway::"gw1") Drupe::Action::"Alert"::response(input: { level: 3, server: "s1" }, callerPrincipal: Drupe::OAuthUser::"s1", callerResource: Drupe::Gateway::"gw1", requestId: "u6")

Expected Output

@0 (time point 0): DENY
@2 (time point 1): DENY
@10 (time point 2): DENY
@12 (time point 3): DENY
@20 (time point 4): DENY
@22 (time point 5): DENY