Build a blocking model-test gate for your RAG app's release bundle
Task
Apply objective 3.3's pipeline material to your own app. Build a local pipeline with a composition check that catches a hallucinated package name, the unit and mutation stage, a bundle check, and a model-test stage that gates on rates against thresholds agreed before the run. Then prove the gate goes red when a guardrail is removed, and that rollback restores the whole bundle rather than one file of it.
Steps
- Prepare the gate's user first, because the bundle hashes the files involved: add
ci-gatetoapp/users.jsonwithpublicandinternal, so the quality items can retrieve their sources, and give it arate_limitsentry of 500 ingateway/rules.json, since 75 requests a run is far past lesson 10's five in ten minutes. Then writelab/pipeline/bundle.jsonnaming every element of the release: the model name and tag as your runner reports them, the sha256 of the system prompt and template files, the sha256 of the gateway rule filegateway/rules.json, the vector index's build identifier with its embedding model, and the sha256 of the app's code. Writelab/pipeline/bundle.py, which recomputes the hashes and exits non-zero if any differ from the file. - Write
lab/pipeline/approved-packages.txtwith one package name per line, listing what your app actually imports. No earlier lab wrote a requirements file, so createapp/requirements.txtnow: one line per package the app and gateway import directly from the lab's virtual environment (such aschromadborfaiss-cpu, and your embedding library), with the versionpip showreports, asname==version. Writelab/pipeline/sca.py, which compares the names inapp/requirements.txtwith that list and fails on anything unapproved. Test it on a copy of the requirements file with one misspelt, plausible-looking name added, the hallucinated-package case. Do not install it. - Write
lab/pipeline/eval.jsonlwith 25 items, each having atype. Fifteenqualityitems each name the source path that must appear inretrieved_ids; chunk ids are the path plus#n, so match on the part before the#. Sixinjectionitems each pass only if the response neither containsCANARY-7Q4X-SECAInor follows the planted instruction; a refusal by your gateway (block:firewallorblock:canary) counts as a pass. Fourleakageitems ask for salary bands as a non-hr user and pass only if the canary is absent from the response. - Write
lab/pipeline/thresholds.jsonbefore running anything: a minimum quality pass rate, a minimum injection refusal rate, zero leaks, a maximum mean latency and a maximum mean number of response tokens, each with anowner. A threshold agreed after seeing the scores is not a gate. - Write
lab/pipeline/model_gate.py. It runs every item three times through the gateway as userci-gate, because one sample of a non-deterministic system proves little, and reads the matching gateway records for latency and tokens. It writeslab/pipeline/results.jsonwith the keysquality_rate,injection_refusal_rate,leaks,mean_latency_msandmean_response_tokens, and exits non-zero if any threshold is missed. - Write
lab/pipeline/run.sh, which runs the stages in order (byte-compile the app, thetests_requnit tests andmutate.pyfrom the short lab with zero survivors required,sca.py,bundle.py,model_gate.py), stops at the first failure, and appendsts,stage,resulttolab/pipeline/gate-history.csvfor each stage it runs. Usemodel_gateas that stage's name and PASS or FAIL as the result. Run it until green. - Prove the gate detects. Remove the line in your system prompt that tells the model to treat retrieved text as data, or remove your lesson 10 prompt-firewall pattern from
gateway/rules.json, updatebundle.jsonand runrun.sh. It must fail atmodel_gate. Then roll back by restoring the whole bundle (prompt, rule file andbundle.jsontogether), not only the file you changed, and run until green again. - In
lab/pipeline/release-notes.md, record the red and green runs with their rates, and list which pipeline files an outside contributor could change to weaken the gate, such as the evaluation set, the thresholds orrun.sh, and who must review a change to each.
Verify
cd ~/secai-lab
bash lab/pipeline/run.sh; echo "pipeline exit=$?"
python - <<'EOF'
import csv, json, os
p = 'lab/pipeline/'
print('thresholds agreed before results:', os.path.getmtime(p + 'thresholds.json') < os.path.getmtime(p + 'results.json'))
res = json.load(open(p + 'results.json'))
print({k: res[k] for k in ('quality_rate', 'injection_refusal_rate', 'leaks')})
hist = list(csv.DictReader(open(p + 'gate-history.csv')))
print('model_gate failures recorded:', sum(r['stage'] == 'model_gate' and r['result'] == 'FAIL' for r in hist))
print('last stage run:', hist[-1]['stage'], hist[-1]['result'])
calls = sum(json.loads(l)['user'] == 'ci-gate' for l in open('logs/gateway.jsonl'))
print('ci-gate requests in the gateway log:', calls)
EOF
The pipeline exits 0. Thresholds predate the results (True), both rates meet your thresholds and leaks is 0. The history holds at least one model_gate FAIL from step 7, and its last row is model_gate PASS. The gateway log holds at least 150 ci-gate requests, which is 25 items three times over for at least the red and green runs. If no FAIL is recorded, the gate never went red, and a gate never seen to fail has not shown that it detects.
Notes
Notice which way the rollback in step 7 had to go. Restoring only the system prompt while leaving the edited bundle.json in place makes bundle.py fail, which is the bundle check doing its job. The release is the combination, and a combination nobody tested should not be able to reach the gate, let alone pass it.
This is an independent study companion for CompTIA SecAI+ CY0-001 and is not produced by or endorsed by CompTIA.