Spaces:
Running
feat(eval): Week 1 step 5 — 25-question K8s golden dataset + grounded_refusal fix
Browse filesAuthor k8s_golden.json with 25 questions mapped to the CRAG 8-type
taxonomy locked in QUESTION_PLAN.md. Distribution: 6 simple,
4 simple_w_condition, 4 comparison, 6 multi_hop, 4 false_premise
(2 flavor A + 2 flavor B), 1 set. 2 questions flagged time_sensitive
(k8s_005 PSA stable-since-v1.25, k8s_018 HPA autoscaling/v2).
Pilot file k8s_golden_pilot.json retained unchanged as session
history (tests/test_golden_schema still asserts against it).
Fix grounded_refusal metric — two bugs, one semantic area.
Bug 1: the metric's docstring said it checks whether the ANSWER
cites no sources, but the implementation was checking whether
retrieval returned zero candidates. Real agents retrieve candidates,
find them irrelevant, and refuse in the answer text with no inline
citations — that's the refusal shape the metric is meant to score.
Fix checks for [source:...] in the answer text instead. Silent
false-negative on all 5 fastapi OOS questions (q008–q010, q026–q027)
which all correctly refuse but were being marked False; the
refusal_rate aggregate in report.py shifts by that 5-question delta.
Bug 2: surfaced during the 25-question functional check. The phrase
list recognized "does not contain information" but missed "not in
the {corpus_label} documentation" — the exact canonical shape taught
by the system prompt at core/prompts.py:17-18. LLM non-determinism
meant k8s_004 and k8s_024 produced the canonical form and were
marked False even though the refusal was correct. Fix adds a narrow
regex `\bnot in the\b[^.]{0,60}\bdocumentation\b` alongside the
phrase list. Rejected substring "not in the" because it would false-
positive on valid retrieval answers like "not in the same scope as"
or "not in the default range". Two unit tests pin both directions —
the negative test is load-bearing so a future refactor cannot
silently widen the matcher back to substring.
Add time_sensitive: bool field to GoldenQuestion schema.
Pre-gate (pilot against expanded 28-page corpus): all 6 pilot
questions retain R@5=1.00, citation=1.00, grounded_refusal=True.
pilot_005 max_score=0.01639 unchanged — 0.015 threshold holds.
First-5 pilot gate: all 5 pass after fixing k8s_003 expected_sources
overspec and the grounded_refusal metric.
Full 25Q functional check (post-fix): Avg P@5=0.83, R@5=0.96,
KHR=0.90; both flavor-A OOS questions (k8s_004, k8s_024) correctly
register as grounded refusals. k8s_002 R@5=0.50 is documented as
reranker-stressor (per QUESTION_PLAN.md). k8s_022 flavor B shows
LLM refusing the documented-negative instead of citing it — same
class as pilot_005, out of scope for this commit.
Scope bound: this commit contains authoring + first-5 pilot gate
+ grounded_refusal correctness fix. Full 25-question threshold
sweep and refusal-threshold calibration against the full set are
a separate session. Routes.py:552 audit-logger still uses the old
broken semantics in an audit record field — deferred to a follow-up.
Tests: 444 passing (+2 for the canonical/substring regex tests).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- DECISIONS.md +61 -0
- agent_bench/evaluation/datasets/k8s_golden.json +534 -0
- agent_bench/evaluation/harness.py +8 -1
- agent_bench/evaluation/metrics.py +23 -9
- agent_bench/langchain_baseline/runner.py +1 -3
- configs/default.yaml +1 -1
- tests/test_evaluation.py +49 -13
|
@@ -1229,6 +1229,67 @@ are real, shipped, measurement-grounded infrastructure changes.
|
|
| 1229 |
The two fix attempts are documented learning that shapes the
|
| 1230 |
future direction.
|
| 1231 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1232 |
**Narrative summary.** Session hypothesis: pilot_005 is a
|
| 1233 |
counterfactual-query-expansion problem. Session evidence: the
|
| 1234 |
hypothesis is correct on retrieval — the target chunk is reachable
|
|
|
|
| 1229 |
The two fix attempts are documented learning that shapes the
|
| 1230 |
future direction.
|
| 1231 |
|
| 1232 |
+
## `grounded_refusal` metric reads answer text, not retrieved sources — 2026-04-14
|
| 1233 |
+
|
| 1234 |
+
**Context.** Week 1 step 5 authoring (25-question K8s golden set). Two
|
| 1235 |
+
flavor-A out-of-scope questions (`k8s_004` Jaeger sidecar, `k8s_024`
|
| 1236 |
+
Envoy xDS ADS) surfaced a pre-existing bug in the
|
| 1237 |
+
`grounded_refusal` metric during the functional check.
|
| 1238 |
+
|
| 1239 |
+
**Bug 1 — wrong signal.** The metric's docstring said it checks
|
| 1240 |
+
whether the answer correctly refuses AND cites no sources, but the
|
| 1241 |
+
implementation was checking `len(response_sources) == 0` where
|
| 1242 |
+
`response_sources` is the *retrieved*-sources list. Real agents
|
| 1243 |
+
retrieve candidates on any non-trivial OOS query (the grounded-refusal
|
| 1244 |
+
gate at tool level only catches the thinnest queries), inspect the
|
| 1245 |
+
candidates, find nothing relevant, and refuse *in the answer text*
|
| 1246 |
+
without citing anything. Checking retrieval emptiness flagged those
|
| 1247 |
+
correct refusals as failures. Fix: inspect the answer text for
|
| 1248 |
+
`[source: X.md]` citations via regex; drop the `response_sources`
|
| 1249 |
+
parameter from the signature entirely.
|
| 1250 |
+
|
| 1251 |
+
This was a silent false negative on all 5 fastapi out-of-scope
|
| 1252 |
+
questions (`q008`–`q010`, `q026`–`q027`) which all correctly refuse
|
| 1253 |
+
but were being marked `grounded_refusal=False`. Aggregate
|
| 1254 |
+
`refusal_rate` in `report.py` shifts by the resulting 5-question
|
| 1255 |
+
delta; any historical comparison to pre-fix fastapi numbers needs
|
| 1256 |
+
to acknowledge this.
|
| 1257 |
+
|
| 1258 |
+
**Bug 2 — metric coverage gap surfaced during 25-question authoring.**
|
| 1259 |
+
`grounded_refusal_rate` recognized "does not contain information"
|
| 1260 |
+
phrasing (in `refusal_phrases` list) but missed "not in the
|
| 1261 |
+
{corpus_label} documentation" phrasing — the exact shape taught by
|
| 1262 |
+
the system prompt at `core/prompts.py:17-18`. The LLM produced the
|
| 1263 |
+
canonical form on some questions and the phrase-list form on others;
|
| 1264 |
+
the metric inflation/deflation was non-deterministic. Fix: narrow
|
| 1265 |
+
regex `\bnot in the\b[^.]{0,60}\bdocumentation\b` added alongside
|
| 1266 |
+
phrase-list matching.
|
| 1267 |
+
|
| 1268 |
+
**Rejected alternative.** Substring `"not in the"` would produce
|
| 1269 |
+
false positives on valid-answer phrasing — "the rate limit is not in
|
| 1270 |
+
the same scope as the request timeout", "the flag is not in the 1.28
|
| 1271 |
+
release; it landed in 1.29", "this value is not in the default
|
| 1272 |
+
range" — all of which are legitimate retrieval answers with
|
| 1273 |
+
conditional or scope-limiting language, not refusals. Honest
|
| 1274 |
+
evaluation cannot afford a metric that silently counts these as
|
| 1275 |
+
grounded refusals.
|
| 1276 |
+
|
| 1277 |
+
**Tests.** Two unit tests pin both directions:
|
| 1278 |
+
`test_canonical_refusal_phrasing_recognized` covers the positive
|
| 1279 |
+
case ("The answer is not in the Kubernetes documentation"), and
|
| 1280 |
+
`test_not_in_the_is_not_substring_refusal` covers the negative case
|
| 1281 |
+
("The rate limit is not in the same scope as the request timeout").
|
| 1282 |
+
The negative test is the load-bearing one — without it, a future
|
| 1283 |
+
refactor could silently widen the matcher back to substring and pass
|
| 1284 |
+
all existing tests. The negative test pins design intent.
|
| 1285 |
+
|
| 1286 |
+
**Scope bound.** This is a metric correctness fix, not a threshold
|
| 1287 |
+
change. The 0.015 refusal-gate threshold (calibrated in `b97f00f`
|
| 1288 |
+
against the 6-question pilot) is unchanged by this commit. Whether
|
| 1289 |
+
the corrected metric shifts the optimal threshold against the full
|
| 1290 |
+
25-question set is a question for the threshold-sweep session, not
|
| 1291 |
+
this authoring session.
|
| 1292 |
+
|
| 1293 |
**Narrative summary.** Session hypothesis: pilot_005 is a
|
| 1294 |
counterfactual-query-expansion problem. Session evidence: the
|
| 1295 |
hypothesis is correct on retrieval — the target chunk is reachable
|
|
@@ -0,0 +1,534 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"corpus": "k8s",
|
| 3 |
+
"version": "v1.31",
|
| 4 |
+
"snapshot_date": "2026-04-14",
|
| 5 |
+
"chunker": {
|
| 6 |
+
"strategy": "recursive",
|
| 7 |
+
"chunk_size": 512,
|
| 8 |
+
"chunk_overlap": 64
|
| 9 |
+
},
|
| 10 |
+
"questions": [
|
| 11 |
+
{
|
| 12 |
+
"id": "k8s_001",
|
| 13 |
+
"question": "What identity guarantees does Kubernetes provide to Pods managed by a StatefulSet?",
|
| 14 |
+
"expected_answer_keywords": ["ordinal", "stable network identity", "stable storage", "sticky"],
|
| 15 |
+
"expected_sources": ["k8s_statefulset.md"],
|
| 16 |
+
"category": "retrieval",
|
| 17 |
+
"difficulty": "easy",
|
| 18 |
+
"requires_calculator": false,
|
| 19 |
+
"reference_answer": "StatefulSet Pods have a unique identity composed of an ordinal index, a stable network identity, and stable persistent storage. The identity sticks to each Pod across (re)scheduling, so a replacement Pod assumes the same identity as the one it replaced \u2014 unlike the interchangeable Pods managed by a Deployment.",
|
| 20 |
+
"question_type": "simple",
|
| 21 |
+
"is_multi_hop": false,
|
| 22 |
+
"time_sensitive": false,
|
| 23 |
+
"source_chunk_ids": ["5214c2336b5cd520"],
|
| 24 |
+
"source_snippets": [
|
| 25 |
+
"StatefulSet Pods have a unique identity that consists of an ordinal, a stable network identity, and stable storage"
|
| 26 |
+
],
|
| 27 |
+
"source_pages": ["concepts/workloads/controllers/statefulset"],
|
| 28 |
+
"source_sections": ["Pod Identity"]
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"id": "k8s_002",
|
| 32 |
+
"question": "How does a StatefulSet differ from a Deployment when managing Pods, and when would you prefer one over the other?",
|
| 33 |
+
"expected_answer_keywords": ["stateless", "sticky identity", "declarative", "interchangeable", "persistent"],
|
| 34 |
+
"expected_sources": ["k8s_deployment.md", "k8s_statefulset.md"],
|
| 35 |
+
"category": "retrieval",
|
| 36 |
+
"difficulty": "medium",
|
| 37 |
+
"requires_calculator": false,
|
| 38 |
+
"reference_answer": "A Deployment manages a set of Pods for an application workload that does not maintain state and provides declarative updates; its Pods are interchangeable replicas. A StatefulSet, by contrast, maintains a sticky identity for each of its Pods \u2014 stable network identifiers, stable persistent storage, and ordered deployment/scaling \u2014 which makes it the right choice when the workload needs per-Pod identity or per-Pod storage.",
|
| 39 |
+
"question_type": "comparison",
|
| 40 |
+
"is_multi_hop": true,
|
| 41 |
+
"time_sensitive": false,
|
| 42 |
+
"source_chunk_ids": ["2a2ff3b0d4346555", "c0d6f7e3674ad4fb"],
|
| 43 |
+
"source_snippets": [
|
| 44 |
+
"A Deployment manages a set of Pods to run an application workload, usually one that doesn't maintain state",
|
| 45 |
+
"Unlike a Deployment, a StatefulSet maintains a sticky identity for each of its Pods"
|
| 46 |
+
],
|
| 47 |
+
"source_pages": [
|
| 48 |
+
"concepts/workloads/controllers/deployment",
|
| 49 |
+
"concepts/workloads/controllers/statefulset"
|
| 50 |
+
],
|
| 51 |
+
"source_sections": ["", ""]
|
| 52 |
+
},
|
| 53 |
+
{
|
| 54 |
+
"id": "k8s_003",
|
| 55 |
+
"question": "How does external HTTP traffic reach a Pod inside a Kubernetes cluster, from the Ingress edge through the Service layer down to the Pod?",
|
| 56 |
+
"expected_answer_keywords": ["Ingress", "HTTP", "Service", "selector", "Pod"],
|
| 57 |
+
"expected_sources": ["k8s_ingress.md", "k8s_service.md"],
|
| 58 |
+
"category": "retrieval",
|
| 59 |
+
"difficulty": "hard",
|
| 60 |
+
"requires_calculator": false,
|
| 61 |
+
"reference_answer": "Ingress exposes HTTP and HTTPS routes from outside the cluster and maps them to backend Services based on rules defined on the Ingress resource. A Service is an abstraction that defines a logical set of endpoints (usually Pods) and uses a selector to decide which Pods to target, load-balancing traffic across them. The Service delivers traffic to the container port each Pod exposes.",
|
| 62 |
+
"question_type": "multi_hop",
|
| 63 |
+
"is_multi_hop": true,
|
| 64 |
+
"time_sensitive": false,
|
| 65 |
+
"source_chunk_ids": [
|
| 66 |
+
"8f8f44037c2580fc",
|
| 67 |
+
"398fda53c7ce840a"
|
| 68 |
+
],
|
| 69 |
+
"source_snippets": [
|
| 70 |
+
"Ingress](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#ingress-v1-networking-k8s-io) exposes HTTP and HTTPS routes from outside the cluster to",
|
| 71 |
+
"The set of Pods targeted by a Service is usually determined by a"
|
| 72 |
+
],
|
| 73 |
+
"source_pages": [
|
| 74 |
+
"concepts/services-networking/ingress",
|
| 75 |
+
"concepts/services-networking/service"
|
| 76 |
+
],
|
| 77 |
+
"source_sections": ["What is Ingress?", ""]
|
| 78 |
+
},
|
| 79 |
+
{
|
| 80 |
+
"id": "k8s_004",
|
| 81 |
+
"question": "How do I enable Jaeger sidecar injection for distributed tracing in a Kubernetes Deployment?",
|
| 82 |
+
"expected_answer_keywords": ["does not", "not contain", "Jaeger"],
|
| 83 |
+
"expected_sources": [],
|
| 84 |
+
"category": "out_of_scope",
|
| 85 |
+
"difficulty": "medium",
|
| 86 |
+
"requires_calculator": false,
|
| 87 |
+
"reference_answer": "The Kubernetes documentation in this corpus does not cover Jaeger, distributed tracing sidecar injection, or observability agent integration. Jaeger is a third-party project that lives outside Kubernetes core docs; the right answer is to refuse and cite zero sources.",
|
| 88 |
+
"question_type": "false_premise",
|
| 89 |
+
"is_multi_hop": false,
|
| 90 |
+
"time_sensitive": false,
|
| 91 |
+
"source_chunk_ids": [],
|
| 92 |
+
"source_snippets": [],
|
| 93 |
+
"source_pages": [],
|
| 94 |
+
"source_sections": []
|
| 95 |
+
},
|
| 96 |
+
{
|
| 97 |
+
"id": "k8s_005",
|
| 98 |
+
"question": "As of Kubernetes v1.31, how does Pod Security Admission behave differently when a namespace is labeled with enforce mode versus warn mode?",
|
| 99 |
+
"expected_answer_keywords": ["enforce", "warn", "rejected", "warning", "namespace"],
|
| 100 |
+
"expected_sources": ["k8s_pod_security_admission.md"],
|
| 101 |
+
"category": "retrieval",
|
| 102 |
+
"difficulty": "medium",
|
| 103 |
+
"requires_calculator": false,
|
| 104 |
+
"reference_answer": "Pod Security Admission (stable since Kubernetes v1.25) applies restrictions at the namespace level based on labels. With enforce mode, policy violations cause the Pod to be rejected at admission. With warn mode, policy violations trigger a user-facing warning but the Pod is still allowed. A namespace can combine modes (for example enforce plus warn) at different levels.",
|
| 105 |
+
"question_type": "simple_w_condition",
|
| 106 |
+
"is_multi_hop": false,
|
| 107 |
+
"time_sensitive": true,
|
| 108 |
+
"source_chunk_ids": ["e6921b9ccdcf4571", "052a900bb777ec1c"],
|
| 109 |
+
"source_snippets": [
|
| 110 |
+
"Policy violations will cause the pod to be rejected",
|
| 111 |
+
"FEATURE STATE: `Kubernetes v1.25 [stable]"
|
| 112 |
+
],
|
| 113 |
+
"source_pages": [
|
| 114 |
+
"concepts/security/pod-security-admission",
|
| 115 |
+
"concepts/security/pod-security-admission"
|
| 116 |
+
],
|
| 117 |
+
"source_sections": ["Pod Security Admission labels for namespaces", ""]
|
| 118 |
+
},
|
| 119 |
+
{
|
| 120 |
+
"id": "k8s_006",
|
| 121 |
+
"question": "What is a ConfigMap in Kubernetes and what kind of data should you store in it?",
|
| 122 |
+
"expected_answer_keywords": ["ConfigMap", "non-confidential", "key-value", "configuration"],
|
| 123 |
+
"expected_sources": ["k8s_configmap.md"],
|
| 124 |
+
"category": "retrieval",
|
| 125 |
+
"difficulty": "easy",
|
| 126 |
+
"requires_calculator": false,
|
| 127 |
+
"reference_answer": "A ConfigMap is an API object used to store non-confidential data in key-value pairs. It is intended for application configuration that does not need to be kept secret. Confidential data such as passwords or tokens should live in a Secret, not a ConfigMap.",
|
| 128 |
+
"question_type": "simple",
|
| 129 |
+
"is_multi_hop": false,
|
| 130 |
+
"time_sensitive": false,
|
| 131 |
+
"source_chunk_ids": ["b6a867a1906a3ff2"],
|
| 132 |
+
"source_snippets": [
|
| 133 |
+
"A ConfigMap is an API object used to store non-confidential data in key-value pairs"
|
| 134 |
+
],
|
| 135 |
+
"source_pages": ["concepts/configuration/configmap"],
|
| 136 |
+
"source_sections": [""]
|
| 137 |
+
},
|
| 138 |
+
{
|
| 139 |
+
"id": "k8s_007",
|
| 140 |
+
"question": "What does a Kubernetes Job do, and how does it decide that its task is complete?",
|
| 141 |
+
"expected_answer_keywords": ["Job", "Pods", "retry", "completions", "terminate"],
|
| 142 |
+
"expected_sources": ["k8s_job.md"],
|
| 143 |
+
"category": "retrieval",
|
| 144 |
+
"difficulty": "easy",
|
| 145 |
+
"requires_calculator": false,
|
| 146 |
+
"reference_answer": "A Job creates one or more Pods and will continue to retry execution of the Pods until a specified number of them successfully terminate. As Pods successfully complete, the Job tracks the successful completions; once the specified number is reached, the Job is considered complete. Deleting a Job cleans up the Pods it created.",
|
| 147 |
+
"question_type": "simple",
|
| 148 |
+
"is_multi_hop": false,
|
| 149 |
+
"time_sensitive": false,
|
| 150 |
+
"source_chunk_ids": ["b704f9dbc8422835"],
|
| 151 |
+
"source_snippets": [
|
| 152 |
+
"A Job creates one or more Pods and will continue to retry execution of the Pods until a specified number of them successfully terminate"
|
| 153 |
+
],
|
| 154 |
+
"source_pages": ["concepts/workloads/controllers/job"],
|
| 155 |
+
"source_sections": [""]
|
| 156 |
+
},
|
| 157 |
+
{
|
| 158 |
+
"id": "k8s_008",
|
| 159 |
+
"question": "What is a Kubernetes Namespace, and which kinds of resources does namespace scoping apply to?",
|
| 160 |
+
"expected_answer_keywords": ["Namespace", "isolating", "unique", "namespaced", "cluster"],
|
| 161 |
+
"expected_sources": ["k8s_namespaces.md"],
|
| 162 |
+
"category": "retrieval",
|
| 163 |
+
"difficulty": "easy",
|
| 164 |
+
"requires_calculator": false,
|
| 165 |
+
"reference_answer": "Namespaces provide a mechanism for isolating groups of resources within a single cluster. Resource names must be unique within a Namespace but not across Namespaces. Namespace-based scoping applies only to namespaced objects such as Deployments and Services \u2014 cluster-wide objects like Nodes, PersistentVolumes, or StorageClass are not namespaced.",
|
| 166 |
+
"question_type": "simple",
|
| 167 |
+
"is_multi_hop": false,
|
| 168 |
+
"time_sensitive": false,
|
| 169 |
+
"source_chunk_ids": ["36dc3e5824f31ef7"],
|
| 170 |
+
"source_snippets": [
|
| 171 |
+
"namespaces* provide a mechanism for isolating groups of resources within a single cluster"
|
| 172 |
+
],
|
| 173 |
+
"source_pages": ["concepts/overview/working-with-objects/namespaces"],
|
| 174 |
+
"source_sections": [""]
|
| 175 |
+
},
|
| 176 |
+
{
|
| 177 |
+
"id": "k8s_009",
|
| 178 |
+
"question": "What are the four object kinds that the Kubernetes RBAC API declares, and what does each one do?",
|
| 179 |
+
"expected_answer_keywords": ["Role", "ClusterRole", "RoleBinding", "ClusterRoleBinding"],
|
| 180 |
+
"expected_sources": ["k8s_rbac.md"],
|
| 181 |
+
"category": "retrieval",
|
| 182 |
+
"difficulty": "easy",
|
| 183 |
+
"requires_calculator": false,
|
| 184 |
+
"reference_answer": "The RBAC API declares four object kinds: Role, ClusterRole, RoleBinding, and ClusterRoleBinding. Role and ClusterRole contain rules that represent a set of permissions; RoleBinding and ClusterRoleBinding grant those roles to users, groups, or service accounts. Role and RoleBinding are namespaced, while ClusterRole and ClusterRoleBinding are cluster-wide.",
|
| 185 |
+
"question_type": "simple",
|
| 186 |
+
"is_multi_hop": false,
|
| 187 |
+
"time_sensitive": false,
|
| 188 |
+
"source_chunk_ids": ["d01964ca8fd11edc"],
|
| 189 |
+
"source_snippets": [
|
| 190 |
+
"The RBAC API declares four kinds of Kubernetes object: *Role*, *ClusterRole*, *RoleBinding* and *ClusterRoleBinding*"
|
| 191 |
+
],
|
| 192 |
+
"source_pages": ["reference/access-authn-authz/rbac"],
|
| 193 |
+
"source_sections": ["API objects"]
|
| 194 |
+
},
|
| 195 |
+
{
|
| 196 |
+
"id": "k8s_010",
|
| 197 |
+
"question": "What is a DaemonSet in Kubernetes, and what kind of workload is it designed for?",
|
| 198 |
+
"expected_answer_keywords": ["DaemonSet", "every node", "copy", "daemon"],
|
| 199 |
+
"expected_sources": ["k8s_daemonset.md"],
|
| 200 |
+
"category": "retrieval",
|
| 201 |
+
"difficulty": "easy",
|
| 202 |
+
"requires_calculator": false,
|
| 203 |
+
"reference_answer": "A DaemonSet ensures that all (or some) Nodes in the cluster run a copy of a given Pod. As nodes are added to the cluster, Pods are added to them; as nodes are removed, those Pods are garbage collected. Typical uses are node-local facilities like cluster storage daemons, log collection, and node monitoring \u2014 anything that should run once per node.",
|
| 204 |
+
"question_type": "simple",
|
| 205 |
+
"is_multi_hop": false,
|
| 206 |
+
"time_sensitive": false,
|
| 207 |
+
"source_chunk_ids": ["5c63fa1dc2d8824f"],
|
| 208 |
+
"source_snippets": [
|
| 209 |
+
"DaemonSet* ensures that all (or some) Nodes run a copy of a Pod"
|
| 210 |
+
],
|
| 211 |
+
"source_pages": ["concepts/workloads/controllers/daemonset"],
|
| 212 |
+
"source_sections": [""]
|
| 213 |
+
},
|
| 214 |
+
{
|
| 215 |
+
"id": "k8s_011",
|
| 216 |
+
"question": "When a Pod consumes a Secret, how does the behavior differ between mounting the Secret as a data volume versus exposing it as environment variables for the container?",
|
| 217 |
+
"expected_answer_keywords": ["Secret", "environment variable", "volume", "mounted", "update"],
|
| 218 |
+
"expected_sources": ["k8s_secret.md"],
|
| 219 |
+
"category": "retrieval",
|
| 220 |
+
"difficulty": "medium",
|
| 221 |
+
"requires_calculator": false,
|
| 222 |
+
"reference_answer": "A Secret can be consumed either by mounting it as a data volume (each key becomes a file in the mount path) or by exposing it as environment variables on the container. Both modes deliver the same underlying data, but a mounted volume receives in-place updates if the Secret changes, whereas environment variables are evaluated at Pod start and do not update after the Pod is running.",
|
| 223 |
+
"question_type": "simple_w_condition",
|
| 224 |
+
"is_multi_hop": false,
|
| 225 |
+
"time_sensitive": false,
|
| 226 |
+
"source_chunk_ids": ["3ae2b5f6828d7a89"],
|
| 227 |
+
"source_snippets": [
|
| 228 |
+
"Secrets can be mounted as data volumes or exposed as"
|
| 229 |
+
],
|
| 230 |
+
"source_pages": ["concepts/configuration/secret"],
|
| 231 |
+
"source_sections": ["Using Secrets"]
|
| 232 |
+
},
|
| 233 |
+
{
|
| 234 |
+
"id": "k8s_012",
|
| 235 |
+
"question": "How does an emptyDir volume behave differently when emptyDir.medium is left as the default versus when it is set to Memory?",
|
| 236 |
+
"expected_answer_keywords": ["emptyDir", "medium", "tmpfs", "Memory", "RAM"],
|
| 237 |
+
"expected_sources": ["k8s_volumes.md"],
|
| 238 |
+
"category": "retrieval",
|
| 239 |
+
"difficulty": "medium",
|
| 240 |
+
"requires_calculator": false,
|
| 241 |
+
"reference_answer": "By default, an emptyDir volume is stored on whatever medium backs the node \u2014 disk, SSD, or network storage, depending on the environment. If you set emptyDir.medium to 'Memory', Kubernetes mounts a tmpfs (RAM-backed filesystem) instead. tmpfs is very fast, but files written there count against the container's memory limit.",
|
| 242 |
+
"question_type": "simple_w_condition",
|
| 243 |
+
"is_multi_hop": false,
|
| 244 |
+
"time_sensitive": false,
|
| 245 |
+
"source_chunk_ids": ["42931a154c8263f2"],
|
| 246 |
+
"source_snippets": [
|
| 247 |
+
"If you set the `emptyDir.medium` field to `\"Memory\"`, Kubernetes mounts a tmpfs"
|
| 248 |
+
],
|
| 249 |
+
"source_pages": ["concepts/storage/volumes"],
|
| 250 |
+
"source_sections": ["emptyDir"]
|
| 251 |
+
},
|
| 252 |
+
{
|
| 253 |
+
"id": "k8s_013",
|
| 254 |
+
"question": "How does the kubelet respond differently to a failing liveness probe versus a failing readiness probe on a container?",
|
| 255 |
+
"expected_answer_keywords": ["liveness", "readiness", "restart", "traffic", "Service"],
|
| 256 |
+
"expected_sources": ["k8s_probes.md"],
|
| 257 |
+
"category": "retrieval",
|
| 258 |
+
"difficulty": "medium",
|
| 259 |
+
"requires_calculator": false,
|
| 260 |
+
"reference_answer": "When a liveness probe fails, the kubelet restarts the container to try to recover from a wedged state like a deadlock. When a readiness probe fails, the container is not restarted; instead, the Pod is marked not-ready and removed from Service load balancers, so traffic stops being routed to it until the probe succeeds again.",
|
| 261 |
+
"question_type": "simple_w_condition",
|
| 262 |
+
"is_multi_hop": false,
|
| 263 |
+
"time_sensitive": false,
|
| 264 |
+
"source_chunk_ids": ["b2e141ce1830ae59", "675641157824749c"],
|
| 265 |
+
"source_snippets": [
|
| 266 |
+
"uses liveness probes to know when to restart a container",
|
| 267 |
+
"uses readiness probes to know when a container is ready to start accepting traffic"
|
| 268 |
+
],
|
| 269 |
+
"source_pages": [
|
| 270 |
+
"tasks/configure-pod-container/configure-liveness-readiness-startup-probes",
|
| 271 |
+
"tasks/configure-pod-container/configure-liveness-readiness-startup-probes"
|
| 272 |
+
],
|
| 273 |
+
"source_sections": ["", ""]
|
| 274 |
+
},
|
| 275 |
+
{
|
| 276 |
+
"id": "k8s_014",
|
| 277 |
+
"question": "What is the difference between a Service of type NodePort and a Service of type LoadBalancer in Kubernetes?",
|
| 278 |
+
"expected_answer_keywords": ["NodePort", "LoadBalancer", "Node", "external", "cloud"],
|
| 279 |
+
"expected_sources": ["k8s_service.md"],
|
| 280 |
+
"category": "retrieval",
|
| 281 |
+
"difficulty": "medium",
|
| 282 |
+
"requires_calculator": false,
|
| 283 |
+
"reference_answer": "A Service of type NodePort exposes the Service on each Node's IP at a static port, making it reachable by connecting to any node IP on that port. A Service of type LoadBalancer exposes the Service externally using an external load balancer \u2014 Kubernetes does not directly provide the load balancer, so you must integrate with a cloud provider or supply one yourself. LoadBalancer is typically implemented on top of NodePort in cloud environments.",
|
| 284 |
+
"question_type": "comparison",
|
| 285 |
+
"is_multi_hop": false,
|
| 286 |
+
"time_sensitive": false,
|
| 287 |
+
"source_chunk_ids": ["3257227cc8ef1c68", "3257227cc8ef1c68"],
|
| 288 |
+
"source_snippets": [
|
| 289 |
+
"Exposes the Service on each Node",
|
| 290 |
+
"Exposes the Service externally using an external load balancer"
|
| 291 |
+
],
|
| 292 |
+
"source_pages": [
|
| 293 |
+
"concepts/services-networking/service",
|
| 294 |
+
"concepts/services-networking/service"
|
| 295 |
+
],
|
| 296 |
+
"source_sections": ["Publishing Services (ServiceTypes)", "Publishing Services (ServiceTypes)"]
|
| 297 |
+
},
|
| 298 |
+
{
|
| 299 |
+
"id": "k8s_015",
|
| 300 |
+
"question": "How does a CronJob differ from a Job in Kubernetes, and when would you reach for one over the other?",
|
| 301 |
+
"expected_answer_keywords": ["Job", "CronJob", "schedule", "repeating", "completion"],
|
| 302 |
+
"expected_sources": ["k8s_job.md", "k8s_cronjob.md"],
|
| 303 |
+
"category": "retrieval",
|
| 304 |
+
"difficulty": "medium",
|
| 305 |
+
"requires_calculator": false,
|
| 306 |
+
"reference_answer": "A Job represents a one-off task that runs to completion and then stops; it creates one or more Pods and retries until a specified number successfully terminate. A CronJob creates Jobs on a repeating schedule written in cron format \u2014 it is meant for regular recurring actions such as backups or report generation. Use a Job for a single batch run, and a CronJob when you need the same Job to run on a recurring schedule.",
|
| 307 |
+
"question_type": "comparison",
|
| 308 |
+
"is_multi_hop": true,
|
| 309 |
+
"time_sensitive": false,
|
| 310 |
+
"source_chunk_ids": ["b704f9dbc8422835", "715c42e9d8a1344e"],
|
| 311 |
+
"source_snippets": [
|
| 312 |
+
"Jobs represent one-off tasks that run to completion and then stop",
|
| 313 |
+
"A CronJob starts one-time Jobs on a repeating schedule"
|
| 314 |
+
],
|
| 315 |
+
"source_pages": [
|
| 316 |
+
"concepts/workloads/controllers/job",
|
| 317 |
+
"concepts/workloads/controllers/cron-jobs"
|
| 318 |
+
],
|
| 319 |
+
"source_sections": ["", ""]
|
| 320 |
+
},
|
| 321 |
+
{
|
| 322 |
+
"id": "k8s_016",
|
| 323 |
+
"question": "What is the key scheduling difference between a Deployment and a DaemonSet for running Pods in a cluster?",
|
| 324 |
+
"expected_answer_keywords": ["DaemonSet", "every node", "Deployment", "replicas", "scheduling"],
|
| 325 |
+
"expected_sources": ["k8s_deployment.md", "k8s_daemonset.md"],
|
| 326 |
+
"category": "retrieval",
|
| 327 |
+
"difficulty": "medium",
|
| 328 |
+
"requires_calculator": false,
|
| 329 |
+
"reference_answer": "A Deployment schedules a configured number of replica Pods onto nodes based on the scheduler's placement decisions; the replica count is fixed by the Deployment spec and is independent of the number of nodes. A DaemonSet instead ensures that all (or some) Nodes run a copy of a Pod, so the effective replica count is tied to the number of matching nodes; as nodes are added the DaemonSet Pods are added with them.",
|
| 330 |
+
"question_type": "comparison",
|
| 331 |
+
"is_multi_hop": true,
|
| 332 |
+
"time_sensitive": false,
|
| 333 |
+
"source_chunk_ids": ["2a2ff3b0d4346555", "5c63fa1dc2d8824f"],
|
| 334 |
+
"source_snippets": [
|
| 335 |
+
"A Deployment manages a set of Pods to run an application workload, usually one that doesn't maintain state",
|
| 336 |
+
"DaemonSet* ensures that all (or some) Nodes run a copy of a Pod"
|
| 337 |
+
],
|
| 338 |
+
"source_pages": [
|
| 339 |
+
"concepts/workloads/controllers/deployment",
|
| 340 |
+
"concepts/workloads/controllers/daemonset"
|
| 341 |
+
],
|
| 342 |
+
"source_sections": ["", ""]
|
| 343 |
+
},
|
| 344 |
+
{
|
| 345 |
+
"id": "k8s_017",
|
| 346 |
+
"question": "When a Pod with init containers starts up, what is the order in which its init containers and regular application containers run, and what guarantees does Kubernetes make about that order?",
|
| 347 |
+
"expected_answer_keywords": ["init container", "run to completion", "before", "application", "order"],
|
| 348 |
+
"expected_sources": ["k8s_init_containers.md"],
|
| 349 |
+
"category": "retrieval",
|
| 350 |
+
"difficulty": "hard",
|
| 351 |
+
"requires_calculator": false,
|
| 352 |
+
"reference_answer": "Init containers run one at a time, in the order they are defined in the Pod spec, and each must run to completion before the next one starts. Only after all init containers have successfully terminated does the kubelet start the Pod's regular application containers. If any init container fails, the Pod restarts according to its restartPolicy and the init sequence begins again. This makes init containers the right place for one-time setup work that must finish before the app starts.",
|
| 353 |
+
"question_type": "multi_hop",
|
| 354 |
+
"is_multi_hop": true,
|
| 355 |
+
"time_sensitive": false,
|
| 356 |
+
"source_chunk_ids": ["48069a8c91f98f5b", "329fd28939ef9a4c"],
|
| 357 |
+
"source_snippets": [
|
| 358 |
+
"Init containers are exactly like regular containers",
|
| 359 |
+
"before the main application container"
|
| 360 |
+
],
|
| 361 |
+
"source_pages": [
|
| 362 |
+
"concepts/workloads/pods/init-containers",
|
| 363 |
+
"concepts/workloads/pods/init-containers"
|
| 364 |
+
],
|
| 365 |
+
"source_sections": ["", ""]
|
| 366 |
+
},
|
| 367 |
+
{
|
| 368 |
+
"id": "k8s_018",
|
| 369 |
+
"question": "As of the current Kubernetes snapshot, which autoscaling API version should you use for a HorizontalPodAutoscaler that scales a Deployment on custom or memory metrics, and why?",
|
| 370 |
+
"expected_answer_keywords": ["HorizontalPodAutoscaler", "autoscaling/v2", "custom metrics", "memory", "stable"],
|
| 371 |
+
"expected_sources": ["k8s_hpa.md"],
|
| 372 |
+
"category": "retrieval",
|
| 373 |
+
"difficulty": "hard",
|
| 374 |
+
"requires_calculator": false,
|
| 375 |
+
"reference_answer": "The current stable HorizontalPodAutoscaler API version is autoscaling/v2, which adds support for scaling on memory and custom metrics beyond the CPU-only autoscaling/v1. The new fields introduced in autoscaling/v2 are preserved as annotations when working with autoscaling/v1, but if you need memory or custom metric scaling for a Deployment or StatefulSet you should use autoscaling/v2 directly.",
|
| 376 |
+
"question_type": "multi_hop",
|
| 377 |
+
"is_multi_hop": true,
|
| 378 |
+
"time_sensitive": true,
|
| 379 |
+
"source_chunk_ids": ["eb3877a460c59fb1", "ec57aa3ce82b78a5"],
|
| 380 |
+
"source_snippets": [
|
| 381 |
+
"HorizontalPodAutoscaler* automatically updates a workload resource",
|
| 382 |
+
"The current stable version can be found in the"
|
| 383 |
+
],
|
| 384 |
+
"source_pages": [
|
| 385 |
+
"tasks/run-application/horizontal-pod-autoscale",
|
| 386 |
+
"tasks/run-application/horizontal-pod-autoscale"
|
| 387 |
+
],
|
| 388 |
+
"source_sections": ["", "API Object"]
|
| 389 |
+
},
|
| 390 |
+
{
|
| 391 |
+
"id": "k8s_019",
|
| 392 |
+
"question": "How does a value stored in a ConfigMap become available to an application running inside a Pod \u2014 what are the mechanisms Kubernetes provides?",
|
| 393 |
+
"expected_answer_keywords": ["ConfigMap", "environment variables", "volume", "mounted", "Pod"],
|
| 394 |
+
"expected_sources": ["k8s_configmap.md"],
|
| 395 |
+
"category": "retrieval",
|
| 396 |
+
"difficulty": "hard",
|
| 397 |
+
"requires_calculator": false,
|
| 398 |
+
"reference_answer": "A ConfigMap can be surfaced to a Pod in two main ways: by exposing specific keys as environment variables on the Pod's containers, or by mounting the ConfigMap as a volume so that each key becomes a file in the mount path. Volume-mounted ConfigMap data can also be updated in place when the ConfigMap changes, whereas environment variables are set at Pod start and do not update until the Pod is restarted.",
|
| 399 |
+
"question_type": "multi_hop",
|
| 400 |
+
"is_multi_hop": true,
|
| 401 |
+
"time_sensitive": false,
|
| 402 |
+
"source_chunk_ids": ["b6a867a1906a3ff2"],
|
| 403 |
+
"source_snippets": [
|
| 404 |
+
"A ConfigMap is an API object used to store non-confidential data in key-value pairs"
|
| 405 |
+
],
|
| 406 |
+
"source_pages": ["concepts/configuration/configmap"],
|
| 407 |
+
"source_sections": [""]
|
| 408 |
+
},
|
| 409 |
+
{
|
| 410 |
+
"id": "k8s_020",
|
| 411 |
+
"question": "By default, is an isolated or non-isolated Pod subject to NetworkPolicy filtering, and how does a NetworkPolicy change that baseline?",
|
| 412 |
+
"expected_answer_keywords": ["NetworkPolicy", "non-isolated", "podSelector", "ingress", "egress"],
|
| 413 |
+
"expected_sources": ["k8s_network_policies.md"],
|
| 414 |
+
"category": "retrieval",
|
| 415 |
+
"difficulty": "hard",
|
| 416 |
+
"requires_calculator": false,
|
| 417 |
+
"reference_answer": "By default, Pods are non-isolated \u2014 they accept traffic from any source. A Pod becomes isolated as soon as any NetworkPolicy in its namespace selects it via podSelector; at that point, only traffic explicitly allowed by the union of NetworkPolicies that select that Pod is permitted. NetworkPolicy rules can target ingress, egress, or both, and the CNI plugin is what enforces the policy \u2014 Kubernetes itself does not.",
|
| 418 |
+
"question_type": "multi_hop",
|
| 419 |
+
"is_multi_hop": true,
|
| 420 |
+
"time_sensitive": false,
|
| 421 |
+
"source_chunk_ids": ["f3630532cd0aacb1", "c5be239e31878572"],
|
| 422 |
+
"source_snippets": [
|
| 423 |
+
"non-isolated",
|
| 424 |
+
"namespaceSelector"
|
| 425 |
+
],
|
| 426 |
+
"source_pages": [
|
| 427 |
+
"concepts/services-networking/network-policies",
|
| 428 |
+
"concepts/services-networking/network-policies"
|
| 429 |
+
],
|
| 430 |
+
"source_sections": ["", ""]
|
| 431 |
+
},
|
| 432 |
+
{
|
| 433 |
+
"id": "k8s_021",
|
| 434 |
+
"question": "How does a CronJob get from a cron schedule string to an actual running Pod \u2014 what objects does Kubernetes create along the way?",
|
| 435 |
+
"expected_answer_keywords": ["CronJob", "schedule", "Job", "Pod", "create"],
|
| 436 |
+
"expected_sources": ["k8s_cronjob.md", "k8s_job.md"],
|
| 437 |
+
"category": "retrieval",
|
| 438 |
+
"difficulty": "hard",
|
| 439 |
+
"requires_calculator": false,
|
| 440 |
+
"reference_answer": "A CronJob is like one line of a crontab \u2014 it creates Jobs on a repeating schedule defined in cron format. At each scheduled time, the CronJob controller instantiates a new Job from the jobTemplate. That Job then creates one or more Pods to run the workload, retrying execution until a specified number of Pods successfully terminate. Deleting the CronJob cleans up the Jobs it created, and deleting a Job cleans up its Pods.",
|
| 441 |
+
"question_type": "multi_hop",
|
| 442 |
+
"is_multi_hop": true,
|
| 443 |
+
"time_sensitive": false,
|
| 444 |
+
"source_chunk_ids": ["715c42e9d8a1344e", "b704f9dbc8422835"],
|
| 445 |
+
"source_snippets": [
|
| 446 |
+
"A CronJob starts one-time Jobs on a repeating schedule",
|
| 447 |
+
"A Job creates one or more Pods and will continue to retry execution of the Pods until a specified number of them successfully terminate"
|
| 448 |
+
],
|
| 449 |
+
"source_pages": [
|
| 450 |
+
"concepts/workloads/controllers/cron-jobs",
|
| 451 |
+
"concepts/workloads/controllers/job"
|
| 452 |
+
],
|
| 453 |
+
"source_sections": ["", ""]
|
| 454 |
+
},
|
| 455 |
+
{
|
| 456 |
+
"id": "k8s_022",
|
| 457 |
+
"question": "How do I write an RBAC deny rule that blocks a specific user from deleting Pods in a namespace?",
|
| 458 |
+
"expected_answer_keywords": ["does not", "deny", "purely additive", "no", "RBAC"],
|
| 459 |
+
"expected_sources": ["k8s_rbac.md"],
|
| 460 |
+
"category": "retrieval",
|
| 461 |
+
"difficulty": "hard",
|
| 462 |
+
"requires_calculator": false,
|
| 463 |
+
"reference_answer": "You can't \u2014 Kubernetes RBAC does not support deny rules. The docs explicitly state that Role and ClusterRole rules are purely additive and there are no 'deny' rules. To prevent a user from deleting Pods you simply do not grant them a Role that contains the delete verb on pods; the absence of permission is the only way to block an action.",
|
| 464 |
+
"question_type": "false_premise",
|
| 465 |
+
"is_multi_hop": false,
|
| 466 |
+
"time_sensitive": false,
|
| 467 |
+
"source_chunk_ids": ["ca6603fcb81b1723"],
|
| 468 |
+
"source_snippets": [
|
| 469 |
+
"purely additive (there are no \"deny\" rules)"
|
| 470 |
+
],
|
| 471 |
+
"source_pages": ["reference/access-authn-authz/rbac"],
|
| 472 |
+
"source_sections": ["Role and ClusterRole"]
|
| 473 |
+
},
|
| 474 |
+
{
|
| 475 |
+
"id": "k8s_023",
|
| 476 |
+
"question": "Which container-isolation restrictions does the Pod Security Standards 'privileged' profile enforce on a Pod?",
|
| 477 |
+
"expected_answer_keywords": ["privileged", "unrestricted", "no restrictions", "absence"],
|
| 478 |
+
"expected_sources": ["k8s_pod_security_standards.md"],
|
| 479 |
+
"category": "retrieval",
|
| 480 |
+
"difficulty": "medium",
|
| 481 |
+
"requires_calculator": false,
|
| 482 |
+
"reference_answer": "The privileged profile enforces none \u2014 it is defined by the absence of restrictions. The docs describe the privileged policy as purposely-open and entirely unrestricted: a Pod running under the privileged profile is allowed to bypass typical container isolation mechanisms (for example, access to the node's host network). If you want actual isolation you have to use the baseline or restricted profile instead.",
|
| 483 |
+
"question_type": "false_premise",
|
| 484 |
+
"is_multi_hop": false,
|
| 485 |
+
"time_sensitive": false,
|
| 486 |
+
"source_chunk_ids": ["164541af6b0ebd85"],
|
| 487 |
+
"source_snippets": [
|
| 488 |
+
"Unrestricted policy"
|
| 489 |
+
],
|
| 490 |
+
"source_pages": ["concepts/security/pod-security-standards"],
|
| 491 |
+
"source_sections": ["Privileged"]
|
| 492 |
+
},
|
| 493 |
+
{
|
| 494 |
+
"id": "k8s_024",
|
| 495 |
+
"question": "How do I configure Envoy xDS aggregated discovery service (ADS) for sidecar proxies managed by a Kubernetes Deployment?",
|
| 496 |
+
"expected_answer_keywords": ["does not", "not contain", "Envoy"],
|
| 497 |
+
"expected_sources": [],
|
| 498 |
+
"category": "out_of_scope",
|
| 499 |
+
"difficulty": "medium",
|
| 500 |
+
"requires_calculator": false,
|
| 501 |
+
"reference_answer": "The Kubernetes documentation in this corpus does not cover Envoy, xDS, or aggregated discovery service (ADS) configuration. Envoy is a third-party proxy typically managed by a service mesh project (not Kubernetes core). The right answer is to refuse and cite zero sources.",
|
| 502 |
+
"question_type": "false_premise",
|
| 503 |
+
"is_multi_hop": false,
|
| 504 |
+
"time_sensitive": false,
|
| 505 |
+
"source_chunk_ids": [],
|
| 506 |
+
"source_snippets": [],
|
| 507 |
+
"source_pages": [],
|
| 508 |
+
"source_sections": []
|
| 509 |
+
},
|
| 510 |
+
{
|
| 511 |
+
"id": "k8s_025",
|
| 512 |
+
"question": "Which Kubernetes Service types expose an application to traffic from outside the cluster?",
|
| 513 |
+
"expected_answer_keywords": ["NodePort", "LoadBalancer", "ExternalName", "Ingress"],
|
| 514 |
+
"expected_sources": ["k8s_service.md"],
|
| 515 |
+
"category": "retrieval",
|
| 516 |
+
"difficulty": "medium",
|
| 517 |
+
"requires_calculator": false,
|
| 518 |
+
"reference_answer": "The Service types that expose an application outside the cluster are NodePort (exposes the Service on each Node's IP at a static port), LoadBalancer (exposes the Service externally using an external load balancer supplied by a cloud integration), and ExternalName (maps the Service to an external DNS name via a CNAME record). ClusterIP is the default and is cluster-internal only; for HTTP/HTTPS routing from outside the cluster, Ingress can front a ClusterIP Service as an alternative to NodePort/LoadBalancer.",
|
| 519 |
+
"question_type": "set",
|
| 520 |
+
"is_multi_hop": false,
|
| 521 |
+
"time_sensitive": false,
|
| 522 |
+
"source_chunk_ids": ["52fd016472117b4b", "3257227cc8ef1c68"],
|
| 523 |
+
"source_snippets": [
|
| 524 |
+
"Exposes the Service on a cluster-internal IP",
|
| 525 |
+
"Exposes the Service externally using an external load balancer"
|
| 526 |
+
],
|
| 527 |
+
"source_pages": [
|
| 528 |
+
"concepts/services-networking/service",
|
| 529 |
+
"concepts/services-networking/service"
|
| 530 |
+
],
|
| 531 |
+
"source_sections": ["Publishing Services (ServiceTypes)", "Publishing Services (ServiceTypes)"]
|
| 532 |
+
}
|
| 533 |
+
]
|
| 534 |
+
}
|
|
@@ -36,6 +36,13 @@ class GoldenQuestion(BaseModel):
|
|
| 36 |
source_snippets: list[str] = []
|
| 37 |
question_type: str = ""
|
| 38 |
is_multi_hop: bool = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
# Authoring-time anchors for pre-ingestion golden datasets; index-aligned
|
| 40 |
# with source_snippets. source_sections[i] == "" means the snippet lives in
|
| 41 |
# page lede content above the first H2/H3 — this is allowed, not a missing
|
|
@@ -130,7 +137,7 @@ async def run_evaluation(
|
|
| 130 |
retrieval_recall=retrieval_recall_at_k(ranked_sources, q.expected_sources),
|
| 131 |
keyword_hit_rate=keyword_hit_rate(agent_response.answer, q.expected_answer_keywords),
|
| 132 |
has_source_citation=source_presence(agent_response),
|
| 133 |
-
grounded_refusal=grounded_refusal(agent_response.answer, q.category
|
| 134 |
citation_accuracy=citation_accuracy(agent_response.answer, deduped_sources),
|
| 135 |
calculator_used_correctly=calculator_used_when_expected(
|
| 136 |
agent_response, q.requires_calculator
|
|
|
|
| 36 |
source_snippets: list[str] = []
|
| 37 |
question_type: str = ""
|
| 38 |
is_multi_hop: bool = False
|
| 39 |
+
# Version-state flag: true when the correct answer depends on a specific
|
| 40 |
+
# K8s (or framework) version / feature-state pin. Orthogonal to
|
| 41 |
+
# question_type — a simple and a simple_w_condition can both be time-
|
| 42 |
+
# sensitive. Defaults false; the v1.1 K8s plan pins 2–3 time_sensitive
|
| 43 |
+
# questions out of 25. The pilot file predates this flag and never sets
|
| 44 |
+
# it, so the default keeps the pilot schema-compatible.
|
| 45 |
+
time_sensitive: bool = False
|
| 46 |
# Authoring-time anchors for pre-ingestion golden datasets; index-aligned
|
| 47 |
# with source_snippets. source_sections[i] == "" means the snippet lives in
|
| 48 |
# page lede content above the first H2/H3 — this is allowed, not a missing
|
|
|
|
| 137 |
retrieval_recall=retrieval_recall_at_k(ranked_sources, q.expected_sources),
|
| 138 |
keyword_hit_rate=keyword_hit_rate(agent_response.answer, q.expected_answer_keywords),
|
| 139 |
has_source_citation=source_presence(agent_response),
|
| 140 |
+
grounded_refusal=grounded_refusal(agent_response.answer, q.category),
|
| 141 |
citation_accuracy=citation_accuracy(agent_response.answer, deduped_sources),
|
| 142 |
calculator_used_correctly=calculator_used_when_expected(
|
| 143 |
agent_response, q.requires_calculator
|
|
@@ -53,16 +53,21 @@ def source_presence(response: AgentResponse) -> bool:
|
|
| 53 |
return len(response.sources) > 0
|
| 54 |
|
| 55 |
|
| 56 |
-
def grounded_refusal(
|
| 57 |
-
answer: str,
|
| 58 |
-
category: str,
|
| 59 |
-
response_sources: list[str],
|
| 60 |
-
) -> bool:
|
| 61 |
"""For out_of_scope: does the answer correctly refuse AND cite no sources?
|
| 62 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
Returns True if:
|
| 64 |
- Category is not out_of_scope (metric not applicable)
|
| 65 |
-
- Category is out_of_scope AND answer contains refusal language AND
|
|
|
|
| 66 |
"""
|
| 67 |
if category != "out_of_scope":
|
| 68 |
return True # not applicable
|
|
@@ -77,9 +82,18 @@ def grounded_refusal(
|
|
| 77 |
"outside the scope",
|
| 78 |
]
|
| 79 |
answer_lower = answer.lower()
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
|
| 84 |
|
| 85 |
def citation_accuracy(answer: str, sources: list[str]) -> float:
|
|
|
|
| 53 |
return len(response.sources) > 0
|
| 54 |
|
| 55 |
|
| 56 |
+
def grounded_refusal(answer: str, category: str) -> bool:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
"""For out_of_scope: does the answer correctly refuse AND cite no sources?
|
| 58 |
|
| 59 |
+
"Cite no sources" means no [source: X.md] citations appear in the answer
|
| 60 |
+
text, not that retrieval returned zero candidates. On any non-trivial
|
| 61 |
+
out-of-scope query, retrieval will still return low-relevance candidates
|
| 62 |
+
(unless the grounded-refusal gate fires at the tool level, which only
|
| 63 |
+
catches the thinnest queries). The agent is expected to inspect the
|
| 64 |
+
candidates, find nothing relevant, and refuse without citing anything —
|
| 65 |
+
and that refusal shape is what this metric measures.
|
| 66 |
+
|
| 67 |
Returns True if:
|
| 68 |
- Category is not out_of_scope (metric not applicable)
|
| 69 |
+
- Category is out_of_scope AND answer contains refusal language AND the
|
| 70 |
+
answer text contains no [source: ...] citations
|
| 71 |
"""
|
| 72 |
if category != "out_of_scope":
|
| 73 |
return True # not applicable
|
|
|
|
| 82 |
"outside the scope",
|
| 83 |
]
|
| 84 |
answer_lower = answer.lower()
|
| 85 |
+
has_phrase_refusal = any(phrase in answer_lower for phrase in refusal_phrases)
|
| 86 |
+
# Canonical shape taught by the system prompt at core/prompts.py:17-18:
|
| 87 |
+
# "not in the {corpus_label} documentation". Narrow regex anchors on
|
| 88 |
+
# "documentation" within 60 chars so plain "not in the" fragments from
|
| 89 |
+
# retrieval answers ("not in the same scope", "not in the default range")
|
| 90 |
+
# do not count as refusals.
|
| 91 |
+
has_canonical_refusal = bool(
|
| 92 |
+
re.search(r"\bnot in the\b[^.]{0,60}\bdocumentation\b", answer, re.IGNORECASE)
|
| 93 |
+
)
|
| 94 |
+
has_refusal = has_phrase_refusal or has_canonical_refusal
|
| 95 |
+
cites_in_answer = re.findall(r"\[source:\s*[^\]]+\]", answer, re.IGNORECASE)
|
| 96 |
+
return has_refusal and len(cites_in_answer) == 0
|
| 97 |
|
| 98 |
|
| 99 |
def citation_accuracy(answer: str, sources: list[str]) -> float:
|
|
@@ -127,9 +127,7 @@ async def run_langchain_evaluation(
|
|
| 127 |
),
|
| 128 |
keyword_hit_rate=keyword_hit_rate(answer, q.expected_answer_keywords),
|
| 129 |
has_source_citation=len(deduped_sources) > 0,
|
| 130 |
-
grounded_refusal=grounded_refusal(
|
| 131 |
-
answer, q.category, deduped_sources
|
| 132 |
-
),
|
| 133 |
citation_accuracy=citation_accuracy(answer, deduped_sources),
|
| 134 |
calculator_used_correctly=(
|
| 135 |
("calculator" in tools_used) if q.requires_calculator else True
|
|
|
|
| 127 |
),
|
| 128 |
keyword_hit_rate=keyword_hit_rate(answer, q.expected_answer_keywords),
|
| 129 |
has_source_citation=len(deduped_sources) > 0,
|
| 130 |
+
grounded_refusal=grounded_refusal(answer, q.category),
|
|
|
|
|
|
|
| 131 |
citation_accuracy=citation_accuracy(answer, deduped_sources),
|
| 132 |
calculator_used_correctly=(
|
| 133 |
("calculator" in tools_used) if q.requires_calculator else True
|
|
@@ -112,5 +112,5 @@ corpora:
|
|
| 112 |
# still holds; full sweep lands with the 25-question golden set.
|
| 113 |
top_k: 5
|
| 114 |
max_iterations: 3
|
| 115 |
-
golden_dataset: agent_bench/evaluation/datasets/
|
| 116 |
available: true
|
|
|
|
| 112 |
# still holds; full sweep lands with the 25-question golden set.
|
| 113 |
top_k: 5
|
| 114 |
max_iterations: 3
|
| 115 |
+
golden_dataset: agent_bench/evaluation/datasets/k8s_golden.json
|
| 116 |
available: true
|
|
@@ -82,28 +82,64 @@ class TestSourcePresence:
|
|
| 82 |
|
| 83 |
|
| 84 |
class TestGroundedRefusal:
|
| 85 |
-
def
|
|
|
|
| 86 |
assert (
|
| 87 |
-
grounded_refusal("The documentation does not contain this info.", "out_of_scope"
|
| 88 |
is True
|
| 89 |
)
|
| 90 |
|
| 91 |
def test_out_of_scope_without_refusal(self):
|
| 92 |
-
assert grounded_refusal("Here is how you do it...", "out_of_scope"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
-
def
|
| 95 |
-
"""
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
"out_of_scope",
|
| 100 |
-
["some_doc.md"],
|
| 101 |
-
)
|
| 102 |
-
is False
|
| 103 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
|
| 105 |
def test_in_scope_always_true(self):
|
| 106 |
-
assert grounded_refusal("any answer", "retrieval"
|
| 107 |
|
| 108 |
|
| 109 |
class TestCitationAccuracy:
|
|
|
|
| 82 |
|
| 83 |
|
| 84 |
class TestGroundedRefusal:
|
| 85 |
+
def test_out_of_scope_with_refusal_no_citations(self):
|
| 86 |
+
"""Refusal phrase + no [source:] citations in answer text = passes."""
|
| 87 |
assert (
|
| 88 |
+
grounded_refusal("The documentation does not contain this info.", "out_of_scope")
|
| 89 |
is True
|
| 90 |
)
|
| 91 |
|
| 92 |
def test_out_of_scope_without_refusal(self):
|
| 93 |
+
assert grounded_refusal("Here is how you do it...", "out_of_scope") is False
|
| 94 |
+
|
| 95 |
+
def test_out_of_scope_refusal_with_citation_in_answer_fails(self):
|
| 96 |
+
"""Refusal phrase + [source:] citation in answer text = NOT a grounded refusal.
|
| 97 |
+
|
| 98 |
+
The metric inspects the answer text for citations rather than the
|
| 99 |
+
retrieved-sources list — a correct flavor-A refusal retrieves
|
| 100 |
+
candidates, inspects them, and explicitly declines to cite any of
|
| 101 |
+
them, which is the behavior the metric is designed to measure.
|
| 102 |
+
"""
|
| 103 |
+
answer = (
|
| 104 |
+
"The documentation does not contain this info. "
|
| 105 |
+
"[source: some_doc.md]"
|
| 106 |
+
)
|
| 107 |
+
assert grounded_refusal(answer, "out_of_scope") is False
|
| 108 |
|
| 109 |
+
def test_out_of_scope_refusal_no_citation_passes_even_with_retrieval(self):
|
| 110 |
+
"""Flavor-A refusal: agent retrieved candidates but answer cites none."""
|
| 111 |
+
answer = (
|
| 112 |
+
"The retrieved context does not contain information about Jaeger "
|
| 113 |
+
"sidecar injection. I cannot provide an answer."
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
)
|
| 115 |
+
# Under the old signature this test would have failed because the
|
| 116 |
+
# retrieved-sources list was non-empty. The fix moves the check to
|
| 117 |
+
# the answer text where the actual citations live.
|
| 118 |
+
assert grounded_refusal(answer, "out_of_scope") is True
|
| 119 |
+
|
| 120 |
+
def test_canonical_refusal_phrasing_recognized(self):
|
| 121 |
+
"""System-prompt-taught shape "not in the {label} documentation" passes.
|
| 122 |
+
|
| 123 |
+
core/prompts.py:17-18 instructs the agent to say "the answer is not
|
| 124 |
+
in the {corpus_label} documentation and stop" on out-of-scope queries.
|
| 125 |
+
The metric must recognize that canonical form.
|
| 126 |
+
"""
|
| 127 |
+
answer = "The answer is not in the Kubernetes documentation."
|
| 128 |
+
assert grounded_refusal(answer, "out_of_scope") is True
|
| 129 |
+
|
| 130 |
+
def test_not_in_the_is_not_substring_refusal(self):
|
| 131 |
+
"""Bare "not in the" fragment must NOT count as refusal.
|
| 132 |
+
|
| 133 |
+
Pins the design choice to match the canonical shape via a narrow
|
| 134 |
+
regex anchored on "documentation" rather than a loose substring.
|
| 135 |
+
A future refactor that widens the matcher to substring "not in the"
|
| 136 |
+
will break this test — that is the point.
|
| 137 |
+
"""
|
| 138 |
+
answer = "The rate limit is not in the same scope as the request timeout."
|
| 139 |
+
assert grounded_refusal(answer, "out_of_scope") is False
|
| 140 |
|
| 141 |
def test_in_scope_always_true(self):
|
| 142 |
+
assert grounded_refusal("any answer", "retrieval") is True
|
| 143 |
|
| 144 |
|
| 145 |
class TestCitationAccuracy:
|