read_heartbeat_since_login_30s
since with a short (seconds) window: the anchor must be recent enough, or the
streak does not count. Permit a Read only if a Heartbeat by the same user
has held continuously since a Login anchor within the last 30 seconds
(since within 30s, with the user pinned via input.user: context.input.user).
The trace (lifted from corpus 0184_since_window_anchor_too_old) shows the
anchor aging out of the 30s window:
@0— alice logs in (thesinceanchor).@10,@20,@30— alice sends Heartbeats, keeping the streak alive.@32— alice Reads. The onlyLoginis now 32s old, past the 30s window, so the anchor no longer counts → deny.@50— another Heartbeat, but still no freshLogin.@52— alice Reads again; the anchor is 52s old → deny.
Every timepoint denies. Non-Read events (Login, Heartbeat) deny because
the permit only applies to action == Read; the Read events deny because the
Login anchor has aged past the 30s window. This all-deny outcome is the point
the guide illustrates: a short since window makes the anchor’s freshness the
deciding factor.
Referenced by guide/04-temporal-expressions.md.
Policy
// `since` with a short (seconds) window: the anchor must be within 30s. Permit
// a Read only if a Heartbeat by the same user has held continuously since a
// Login anchor within the last 30 seconds.
@id("read_heartbeat_since_login_30s")
permit (
principal,
action == Drupe::Action::"Read",
resource
)
when temporal {
Drupe::Action::"Heartbeat"::request{ input.user: context.input.user }
since within 30s
Drupe::Action::"Login"::request{ input.user: context.input.user }
};
Schema
namespace Drupe {
type ContentFilterFinding = {
severityScore: decimal
};
type HeartbeatInput = {
server: String,
user: 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 "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::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { server: "s1", user: "alice" }) Drupe::Action::"Login"::request(input: { server: "s1", user: "alice" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u1")
@10 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { server: "s1", user: "alice" }) Drupe::Action::"Heartbeat"::request(input: { server: "s1", user: "alice" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u2")
@20 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { server: "s1", user: "alice" }) Drupe::Action::"Heartbeat"::request(input: { server: "s1", user: "alice" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u3")
@30 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { server: "s1", user: "alice" }) Drupe::Action::"Heartbeat"::request(input: { server: "s1", user: "alice" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u4")
@32 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { document: "doc1", user: "alice" }) Drupe::Action::"Read"::request(input: { document: "doc1", user: "alice" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u5")
@50 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { server: "s1", user: "alice" }) Drupe::Action::"Heartbeat"::request(input: { server: "s1", user: "alice" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u6")
@52 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { document: "doc2", user: "alice" }) Drupe::Action::"Read"::request(input: { document: "doc2", user: "alice" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u7")
Expected Output
@0 (time point 0): DENY
@10 (time point 1): DENY
@20 (time point 2): DENY
@30 (time point 3): DENY
@32 (time point 4): DENY
@50 (time point 5): DENY
@52 (time point 6): DENY