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_exactly_three_transfers

Counting over a *-wildcard field: permit an Alert only if exactly three Transfers — regardless of amount — occurred within the last hour. The input.amount: * wildcard matches any transfer and binds nothing; tp(t) keeps the rows one-per-timepoint so count tallies distinct past timepoints.

The trace demonstrates both verdicts, including the window boundary:

  • @200 — only two Transfers so far (w1@0, w2@100) → deny (count = 2).
  • @400 — three Transfers in window (w1, w2, w3@300) → allow (count = 3).
  • @600 — a fourth Transfer (w4@500) pushes the count to 4 → deny.
  • @3700 — one hour later, w1@0 has aged out of the 1h window but w2@100, w3@300, and w4@500 remain → count back to 3 → allow (the window-boundary demo).

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

Policy

// Counting over a `*`-wildcard field (matches anything, binds nothing).
// Permit an Alert only if EXACTLY three Transfers, regardless of amount,
// occurred within the last hour.
@id("alert_exactly_three_transfers")
permit (
    principal,
    action == Drupe::Action::"Alert",
    resource
)
when temporal {
    (count for (t: Timepoint). where (
        formerly within 1h (Drupe::Action::"Transfer"::request{ input.amount: * } && tp(t))
    )) == 3
};

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: 5, user: "alice" }) Drupe::Action::"Transfer"::request(input: { amount: 5, user: "alice" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "w1")
@100 scope(principal: Drupe::OAuthUser::"bob", resource: Drupe::Gateway::"gw1") request_context(input: { amount: 42, user: "bob" }) Drupe::Action::"Transfer"::request(input: { amount: 42, user: "bob" }, callerPrincipal: Drupe::OAuthUser::"bob", callerResource: Drupe::Gateway::"gw1", requestId: "w2")
@200 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { level: 1, server: "s1" }) Drupe::Action::"Alert"::request(input: { level: 1, server: "s1" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "a1")
@300 scope(principal: Drupe::OAuthUser::"carol", resource: Drupe::Gateway::"gw1") request_context(input: { amount: 9, user: "carol" }) Drupe::Action::"Transfer"::request(input: { amount: 9, user: "carol" }, callerPrincipal: Drupe::OAuthUser::"carol", callerResource: Drupe::Gateway::"gw1", requestId: "w3")
@400 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { level: 1, server: "s1" }) Drupe::Action::"Alert"::request(input: { level: 1, server: "s1" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "a2")
@500 scope(principal: Drupe::OAuthUser::"dave", resource: Drupe::Gateway::"gw1") request_context(input: { amount: 1, user: "dave" }) Drupe::Action::"Transfer"::request(input: { amount: 1, user: "dave" }, callerPrincipal: Drupe::OAuthUser::"dave", callerResource: Drupe::Gateway::"gw1", requestId: "w4")
@600 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { level: 2, server: "s2" }) Drupe::Action::"Alert"::request(input: { level: 2, server: "s2" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "a3")
@3700 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { level: 1, server: "s1" }) Drupe::Action::"Alert"::request(input: { level: 1, server: "s1" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "a4")

Expected Output

@0 (time point 0): DENY
@100 (time point 1): DENY
@200 (time point 2): DENY
@300 (time point 3): DENY
@400 (time point 4): ALLOW  [rules: 0]
@500 (time point 5): DENY
@600 (time point 6): DENY
@3700 (time point 7): ALLOW  [rules: 0]

Event Schema

// Unpinned event schema.
//
// Same structure as the pinned default but WITHOUT pins. Temporal
// predicates use global-trace semantics: a policy can match events
// from any principal unless it explicitly constrains callerPrincipal.
//
// Use this to see the behavioral difference vs the pinned default:
//   dogwood replay --event-schema configuration/event-schemas/unpinned.dwschema ...

decision event <A>::request {
    ...inputs(A),
    callerPrincipal:   principalType(A),
    callerResource:    resourceType(A),
    requestId:         String,
    sessionId:         String,
}

event <A>::response {
    ...inputs(A),
    ...outputs(A),
    callerPrincipal:   principalType(A),
    callerResource:    resourceType(A),
    requestId:         String,
    sessionId:         String,
}

event <A>::error {
    ...inputs(A),
    callerPrincipal:   principalType(A),
    callerResource:    resourceType(A),
    requestId:         String,
    sessionId:         String,
}