Cap the Time Window of Datadog Searches
Keeps Datadog searches to a recent window. The agent can search logs, spans, RUM, and events as much as it likes, but each call looks back at most 7 days .
On this page
What it does
Keeps Datadog searches to a recent window. The agent can search logs, spans, RUM, and events as much as it likes, but each call looks back at most 7 days. One broad prompt like "find every log for this user" can't quietly turn into a scan of months of data.
This is a transform, not a block: when to minus from is longer than 7 days, from is
moved forward so the window is exactly 7 days. When to is missing or now, the new from
is written as now-7d; otherwise it's an RFC 3339 timestamp 7 days before to. Windows of
7 days or less are left alone, and every other argument passes through unchanged.
The number of records returned isn't capped here. Datadog already bounds each response with
max_tokens (hard cap 20,000), and an agent reading a burst of errors around a deploy
should be able to see them.
Applies to search_datadog_logs, analyze_datadog_logs, search_datadog_spans,
aggregate_spans, search_datadog_rum_events, aggregate_rum_events,
search_datadog_events, and aggregate_events.
Compliance alignment
- GDPR Art. 5(1)(c) — data minimisation: logs often carry personal data, and each agent read covers a bounded period.
- SOC 2 CC6.1 — limits how much history the agent channel can reach per call.
Why ingress
The time range is a request argument; trimming it before the query runs means Datadog never runs the oversized query.
Time formats understood
now, relative now-<n><unit> with units s, m, h, d, w; RFC 3339 timestamps
(2026-10-01T00:00:00Z); dates (2026-10-01); and Unix timestamps in seconds (10 digits)
or milliseconds (13 digits). A from the policy can't parse is left unchanged.
Configuration
max_window_days at the top of the policy. Raise the window to 14 or 30
days if your incident reviews routinely look further back.
Examples
Window trimmed
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "datadog-search_datadog_logs", "type": "tool" },
"payload": { "args": { "query": "service:checkout-api status:error", "from": "now-30d" } }
}
}
allow = true, transform.transformed_payload has from: "now-7d".
Unchanged
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "datadog-aggregate_spans", "type": "tool" },
"payload": { "args": { "query": "service:checkout-api", "from": "now-4h" } }
}
}
allow = true, no transform.
Composition
Pairs with datadog/read-only-except-notebooks
and datadog/redact-secrets-egress.
Known limitations
- Server name prefix. Scope is tools whose name starts with
datadog. Change_is_datadogif your server is registered under another name. - Unparseable times pass. Formats outside the list above are not rewritten.
- Tool defaults. When
fromis omitted, Datadog's default applies (1 hour for most tools, 7 days for RUM), which is already within the cap.
Compliance note. This policy supports alignment with the cited framework controls on the MCP path only. No policy or bundle makes an organization compliant with any framework; web-UI, native-API, and in-app access are outside the gateway's reach by design. Validate against your own compliance program before relying on it.
Policy source (Rego)
package datadog.ingress.cap_search_window
# Transform-only policy — never denies, only bounds how far back a search looks.
default allow := true
max_window_days := 7
_day_ns := 86400000000000
_max_window_ns := max_window_days * _day_ns
_unit_ns := {
"s": 1000000000,
"m": 60000000000,
"h": 3600000000000,
"d": 86400000000000,
"w": 604800000000000,
}
# Normalize separators so `search_datadog_logs` and `search-datadog-logs` match alike.
_tool := replace(lower(object.get(input.resource, "name", "")), "-", "_")
_args := object.get(object.get(input, "payload", {}), "args", {})
_is_datadog if startswith(_tool, "datadog")
_windowed_suffixes := {
"search_datadog_logs", "analyze_datadog_logs",
"search_datadog_spans", "aggregate_spans",
"search_datadog_rum_events", "aggregate_rum_events",
"search_datadog_events", "aggregate_events",
}
_is_windowed if {
_is_datadog
some s in _windowed_suffixes
endswith(_tool, s)
}
_now := time.now_ns()
# --- Time parsing: returns nanoseconds since epoch, undefined if unparseable ---
_parse_time(s) := _now if {
is_string(s)
lower(trim_space(s)) == "now"
}
_parse_time(s) := ns if {
is_string(s)
m := regex.find_all_string_submatch_n(`^now-(\d+)([smhdw])$`, lower(trim_space(s)), 1)
count(m) == 1
ns := _now - (to_number(m[0][1]) * _unit_ns[m[0][2]])
}
_parse_time(s) := ns if {
is_string(s)
regex.match(`^\d{4}-\d{2}-\d{2}T`, trim_space(s))
ns := time.parse_rfc3339_ns(trim_space(s))
}
_parse_time(s) := ns if {
is_string(s)
regex.match(`^\d{4}-\d{2}-\d{2}$`, trim_space(s))
ns := time.parse_ns("2006-01-02", trim_space(s))
}
_parse_time(s) := ns if {
is_string(s)
regex.match(`^\d{10}$`, trim_space(s))
ns := to_number(trim_space(s)) * 1000000000
}
_parse_time(s) := ns if {
is_string(s)
regex.match(`^\d{13}$`, trim_space(s))
ns := to_number(trim_space(s)) * 1000000
}
# --- Window ---
_raw_to := object.get(_args, "to", "now")
_to_is_now if lower(trim_space(_raw_to)) == "now"
_to_ns := _parse_time(_raw_to)
_from_ns := _parse_time(object.get(_args, "from", null))
_window_too_wide if {
_is_windowed
_to_ns - _from_ns > _max_window_ns
}
_new_from := sprintf("now-%dd", [max_window_days]) if _to_is_now
_new_from := time.format([_to_ns - _max_window_ns, "UTC", "2006-01-02T15:04:05Z07:00"]) if not _to_is_now
# --- Transform ---
transform := {"transformed_payload": object.union(_args, {"from": _new_from})} if {
input.action == "tool_pre_invoke"
_window_too_wide
} Canonical source: policy.md on GitHub · raw · raw on this site (.md)