heartbeat_scope_alias
formerly with scope-alias correlation. Permit an Alert only if a
Heartbeat for the same server and the same request scope fired
within the last hour (formerly within 1h).
The scope aliases context.principal / context.resource resolve to the
current request’s scope entities and are pinned against the reserved event
fields callerPrincipal / callerResource, so a heartbeat only counts if
it came from the same principal on the same resource — not just any heartbeat
for that server.
The trace shows both outcomes:
@0—machine-asends aHeartbeatfors1(history-only event; noAlertpermit applies, so the decision is a deny).@100—machine-araises anAlertfors1→ allow (its own heartbeat, same server, same principal/resource, is within the window).@200—machine-braises anAlertfors1→ deny: the server matches, but the scope-alias pins oncallerPrincipalfail (the only heartbeat came frommachine-a).
Referenced by guide/04-temporal-expressions.md.
Policy
// Scope correlation: `principal`/`resource` are the request's scope entities
// (first-class temporal term roots, matching Cedar) and pin against the
// reserved event fields callerPrincipal / callerResource. Permit an Alert
// only if a Heartbeat for the same server, principal, and resource fired within
// the last hour.
@id("alert_recent_heartbeat")
permit (
principal,
action == Drupe::Action::"Alert",
resource
)
when temporal {
formerly within 1h Drupe::Action::"Heartbeat"::request{
input.server: context.input.server,
callerPrincipal: principal,
callerResource: resource
}
};
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 ReadInput = {
document: String,
user: String
};
type ReadOutput = { };
type SensitiveInfoFinding = {
confidenceScore: decimal
};
type SystemContext = {
now: datetime
};
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 "Read" in [Action::"CallTool"] appliesTo {
principal: [IamEntity, OAuthUser, UnauthenticatedUser],
resource: [Gateway],
context: {
input: ReadInput,
output?: ReadOutput,
system: SystemContext
}
};
action "UnknownTool" in [Action::"CallTool"] appliesTo {
principal: [OAuthUser, IamEntity, UnauthenticatedUser],
resource: [Gateway],
context: {
system: SystemContext
}
};
}
Trace
@0 scope(principal: Drupe::OAuthUser::"machine-a", resource: Drupe::Gateway::"gw1") request_context(input: { server: "s1" }) Drupe::Action::"Heartbeat"::request(input: { server: "s1" }, callerPrincipal: Drupe::OAuthUser::"machine-a", callerResource: Drupe::Gateway::"gw1", requestId: "u1")
@100 scope(principal: Drupe::OAuthUser::"machine-a", resource: Drupe::Gateway::"gw1") request_context(input: { level: 3, server: "s1" }) Drupe::Action::"Alert"::request(input: { level: 3, server: "s1" }, callerPrincipal: Drupe::OAuthUser::"machine-a", callerResource: Drupe::Gateway::"gw1", requestId: "u2")
@200 scope(principal: Drupe::OAuthUser::"machine-b", resource: Drupe::Gateway::"gw1") request_context(input: { level: 2, server: "s1" }) Drupe::Action::"Alert"::request(input: { level: 2, server: "s1" }, callerPrincipal: Drupe::OAuthUser::"machine-b", callerResource: Drupe::Gateway::"gw1", requestId: "u3")
Expected Output
@0 (time point 0): DENY
@100 (time point 1): ALLOW [rules: 0]
@200 (time point 2): DENY