|
2504 | 2504 | } |
2505 | 2505 | } |
2506 | 2506 | }, |
| 2507 | + "/v1/projects/{project_id}/policy-replays": { |
| 2508 | + "parameters": [ |
| 2509 | + { |
| 2510 | + "$ref": "#/components/parameters/ProjectId" |
| 2511 | + } |
| 2512 | + ], |
| 2513 | + "post": { |
| 2514 | + "operationId": "replayPolicy", |
| 2515 | + "x-feature-flag": "agent-gateway", |
| 2516 | + "summary": "Replay recorded agent traffic against a policy", |
| 2517 | + "description": "Replays the project's recorded agent audit traffic against a candidate policy (either a draft supplied inline or an existing policy referenced by id) and reports what would change: which queries that ran would now be blocked, which blocked queries would now be permitted, and which results would be masked or row-filtered. Traffic is deduplicated by normalized query shape, newest first, and every statement is evaluated through the data plane's own policy engine, so the verdicts match real enforcement. Stateful checks (budgets, approvals, write-mode routing) are reported as informational notes on each result, not verdicts. This endpoint reads only the audit log; it never connects to the upstream database and persists nothing.", |
| 2518 | + "tags": [ |
| 2519 | + "Policies" |
| 2520 | + ], |
| 2521 | + "requestBody": { |
| 2522 | + "required": true, |
| 2523 | + "content": { |
| 2524 | + "application/json": { |
| 2525 | + "schema": { |
| 2526 | + "$ref": "#/components/schemas/PolicyReplayInput" |
| 2527 | + } |
| 2528 | + } |
| 2529 | + } |
| 2530 | + }, |
| 2531 | + "responses": { |
| 2532 | + "200": { |
| 2533 | + "description": "The replay summary and per-query decisions.", |
| 2534 | + "content": { |
| 2535 | + "application/json": { |
| 2536 | + "schema": { |
| 2537 | + "$ref": "#/components/schemas/PolicyReplayResult" |
| 2538 | + } |
| 2539 | + } |
| 2540 | + } |
| 2541 | + }, |
| 2542 | + "400": { |
| 2543 | + "$ref": "#/components/responses/BadRequest" |
| 2544 | + }, |
| 2545 | + "401": { |
| 2546 | + "$ref": "#/components/responses/Unauthorized" |
| 2547 | + }, |
| 2548 | + "403": { |
| 2549 | + "$ref": "#/components/responses/Forbidden" |
| 2550 | + }, |
| 2551 | + "404": { |
| 2552 | + "$ref": "#/components/responses/NotFound" |
| 2553 | + }, |
| 2554 | + "429": { |
| 2555 | + "$ref": "#/components/responses/TooManyRequests" |
| 2556 | + } |
| 2557 | + } |
| 2558 | + } |
| 2559 | + }, |
2507 | 2560 | "/v1/projects": { |
2508 | 2561 | "post": { |
2509 | 2562 | "operationId": "createProject", |
|
7148 | 7201 | } |
7149 | 7202 | } |
7150 | 7203 | }, |
| 7204 | + "PolicyReplayInput": { |
| 7205 | + "type": "object", |
| 7206 | + "description": "A window of recorded agent traffic to replay plus the candidate policy to replay it against. Supply exactly one of policy_id (an existing saved policy) or policy (an unsaved draft, e.g. the in-progress editor form).", |
| 7207 | + "properties": { |
| 7208 | + "policy_id": { |
| 7209 | + "type": "string", |
| 7210 | + "description": "ID of an existing saved policy profile to replay against.", |
| 7211 | + "example": "pol_01h455vb4pex5vsknk084sn02q" |
| 7212 | + }, |
| 7213 | + "policy": { |
| 7214 | + "$ref": "#/components/schemas/PolicyProfileInput" |
| 7215 | + }, |
| 7216 | + "credential_id": { |
| 7217 | + "type": "string", |
| 7218 | + "description": "Restrict the replay to traffic recorded for one agent credential.", |
| 7219 | + "example": "cred_01h455vb4pex5vsknk084sn02q" |
| 7220 | + }, |
| 7221 | + "start_ts": { |
| 7222 | + "type": "string", |
| 7223 | + "format": "date-time", |
| 7224 | + "description": "Start of the traffic window (inclusive). Defaults to 7 days before end_ts." |
| 7225 | + }, |
| 7226 | + "end_ts": { |
| 7227 | + "type": "string", |
| 7228 | + "format": "date-time", |
| 7229 | + "description": "End of the traffic window (exclusive). Defaults to now." |
| 7230 | + }, |
| 7231 | + "limit": { |
| 7232 | + "type": "integer", |
| 7233 | + "minimum": 1, |
| 7234 | + "maximum": 500, |
| 7235 | + "default": 200, |
| 7236 | + "description": "Maximum number of distinct queries to replay, newest first. Traffic is deduplicated by normalized query hash before evaluation." |
| 7237 | + } |
| 7238 | + } |
| 7239 | + }, |
| 7240 | + "PolicyReplaySummary": { |
| 7241 | + "type": "object", |
| 7242 | + "description": "Aggregate outcome of a policy replay.", |
| 7243 | + "required": [ |
| 7244 | + "entries_scanned", |
| 7245 | + "distinct_queries", |
| 7246 | + "evaluated", |
| 7247 | + "would_allow", |
| 7248 | + "would_block", |
| 7249 | + "would_mask", |
| 7250 | + "would_row_filter", |
| 7251 | + "newly_blocked", |
| 7252 | + "newly_allowed", |
| 7253 | + "skipped_unparseable", |
| 7254 | + "truncated" |
| 7255 | + ], |
| 7256 | + "properties": { |
| 7257 | + "entries_scanned": { |
| 7258 | + "type": "integer", |
| 7259 | + "format": "int64", |
| 7260 | + "description": "Recorded audit entries covered by the replayed queries." |
| 7261 | + }, |
| 7262 | + "distinct_queries": { |
| 7263 | + "type": "integer", |
| 7264 | + "description": "Distinct normalized queries found in the window (capped at limit)." |
| 7265 | + }, |
| 7266 | + "evaluated": { |
| 7267 | + "type": "integer", |
| 7268 | + "description": "Distinct queries actually evaluated against the candidate policy." |
| 7269 | + }, |
| 7270 | + "would_allow": { |
| 7271 | + "type": "integer", |
| 7272 | + "description": "Queries the candidate policy would allow unchanged." |
| 7273 | + }, |
| 7274 | + "would_block": { |
| 7275 | + "type": "integer", |
| 7276 | + "description": "Queries the candidate policy would block." |
| 7277 | + }, |
| 7278 | + "would_mask": { |
| 7279 | + "type": "integer", |
| 7280 | + "description": "Queries the candidate policy would mask result columns on." |
| 7281 | + }, |
| 7282 | + "would_row_filter": { |
| 7283 | + "type": "integer", |
| 7284 | + "description": "Queries the candidate policy would inject a row filter into." |
| 7285 | + }, |
| 7286 | + "newly_blocked": { |
| 7287 | + "type": "integer", |
| 7288 | + "description": "Queries that executed under the recorded policy but the candidate policy would block." |
| 7289 | + }, |
| 7290 | + "newly_allowed": { |
| 7291 | + "type": "integer", |
| 7292 | + "description": "Queries that were blocked when recorded but the candidate policy would permit." |
| 7293 | + }, |
| 7294 | + "skipped_unparseable": { |
| 7295 | + "type": "integer", |
| 7296 | + "description": "Distinct queries skipped because the stored statement was truncated at record time and can no longer be parsed faithfully." |
| 7297 | + }, |
| 7298 | + "truncated": { |
| 7299 | + "type": "boolean", |
| 7300 | + "description": "True when the window contained more distinct queries than limit; only the newest were replayed." |
| 7301 | + } |
| 7302 | + } |
| 7303 | + }, |
| 7304 | + "PolicyReplayItem": { |
| 7305 | + "type": "object", |
| 7306 | + "description": "One distinct recorded query and the decision the candidate policy would reach for it.", |
| 7307 | + "required": [ |
| 7308 | + "query_hash", |
| 7309 | + "sql", |
| 7310 | + "occurrences", |
| 7311 | + "first_seen", |
| 7312 | + "last_seen", |
| 7313 | + "recorded_event", |
| 7314 | + "change", |
| 7315 | + "result" |
| 7316 | + ], |
| 7317 | + "properties": { |
| 7318 | + "query_hash": { |
| 7319 | + "type": "string", |
| 7320 | + "description": "Normalized query hash identifying this statement shape." |
| 7321 | + }, |
| 7322 | + "sql": { |
| 7323 | + "type": "string", |
| 7324 | + "description": "The recorded statement (normalized, literals replaced with $N placeholders), from its most recent occurrence." |
| 7325 | + }, |
| 7326 | + "statement_kind": { |
| 7327 | + "type": "string", |
| 7328 | + "description": "Statement kind (select, insert, update, delete, ddl, ...)." |
| 7329 | + }, |
| 7330 | + "occurrences": { |
| 7331 | + "type": "integer", |
| 7332 | + "format": "int64", |
| 7333 | + "description": "How many recorded entries share this query shape in the window." |
| 7334 | + }, |
| 7335 | + "first_seen": { |
| 7336 | + "type": "string", |
| 7337 | + "format": "date-time", |
| 7338 | + "description": "Oldest occurrence in the window." |
| 7339 | + }, |
| 7340 | + "last_seen": { |
| 7341 | + "type": "string", |
| 7342 | + "format": "date-time", |
| 7343 | + "description": "Newest occurrence in the window." |
| 7344 | + }, |
| 7345 | + "recorded_event": { |
| 7346 | + "type": "string", |
| 7347 | + "description": "The most recent recorded outcome for this query (query, masked, truncated, or blocked)." |
| 7348 | + }, |
| 7349 | + "recorded_rule": { |
| 7350 | + "type": "string", |
| 7351 | + "description": "The decision rule recorded with a blocked outcome." |
| 7352 | + }, |
| 7353 | + "change": { |
| 7354 | + "type": "string", |
| 7355 | + "enum": [ |
| 7356 | + "newly_blocked", |
| 7357 | + "newly_allowed", |
| 7358 | + "still_allowed", |
| 7359 | + "still_blocked" |
| 7360 | + ], |
| 7361 | + "description": "How the candidate decision compares to the recorded outcome. newly_blocked and newly_allowed are the changes worth reviewing before rollout; still_allowed and still_blocked are unchanged." |
| 7362 | + }, |
| 7363 | + "result": { |
| 7364 | + "$ref": "#/components/schemas/DryEvalResult" |
| 7365 | + } |
| 7366 | + } |
| 7367 | + }, |
| 7368 | + "PolicyReplayResult": { |
| 7369 | + "type": "object", |
| 7370 | + "description": "The outcome of replaying recorded agent traffic against a candidate policy: an aggregate summary plus per-query decisions, changes first.", |
| 7371 | + "required": [ |
| 7372 | + "summary", |
| 7373 | + "items" |
| 7374 | + ], |
| 7375 | + "properties": { |
| 7376 | + "summary": { |
| 7377 | + "$ref": "#/components/schemas/PolicyReplaySummary" |
| 7378 | + }, |
| 7379 | + "items": { |
| 7380 | + "type": "array", |
| 7381 | + "description": "Per-query replay decisions, ordered changes first (newly blocked, then newly allowed), then by occurrence count.", |
| 7382 | + "items": { |
| 7383 | + "$ref": "#/components/schemas/PolicyReplayItem" |
| 7384 | + } |
| 7385 | + } |
| 7386 | + } |
| 7387 | + }, |
7151 | 7388 | "DryEvalMaskedColumn": { |
7152 | 7389 | "type": "object", |
7153 | 7390 | "description": "One result column that would be masked, with the mask kind applied.", |
|
0 commit comments