Checkpoint Lab — GitLab Duo and Agentic DevSecOps: AI Features, Security Remediation, Governance, and Usage Controls
Complete a governance-first agentic DevSecOps checkpoint using synthetic code, tool policies, proposals, audit artifacts, and usage data; reject unsafe work and produce an evidence-backed approval record.
Learning objectives
- Create a complete synthetic governance package for one AI-assisted security-remediation workflow.
- Predict and enforce tool decisions before any simulated mutation occurs, including at least one explicit deny.
- Independently test two remediation proposals and reject the plausible but unsafe candidate with reproducible evidence.
- Produce an approval/audit record that binds data classification, tool decisions, proposal identity, reviewer decision, and zero-credit fixture assumptions.
- Carry the resulting governance pattern into Chapter 34’s secure production GitLab platform capstone.
1. Mission and acceptance criteria
You are the platform reviewer for PROJECT_X. A
synthetic AI session proposes a remediation for a template
path-containment flaw. You must decide which data the AI was allowed
to see, which tools it may invoke, which proposed patch is safe, and
what evidence is required before a human authorizes the simulated
write.
CHECKPOINT ACCEPTANCE
[ ] original fixtures preserved
[ ] data classification documented before prompt/tool analysis
[ ] tool decisions predicted before evaluator runs
[ ] at least one write requires approval
[ ] at least one destructive/external operation is denied
[ ] safe and unsafe proposals tested independently
[ ] unsafe plausible proposal rejected with evidence
[ ] approved proposal hash recorded
[ ] audit/session/usage record contains no real secrets or private data
[ ] credits assumption explicitly says fixture / zero live usage
[ ] cleanup is reviewed before deletion
2. Build the checkpoint package
from pathlib import Path
import json, textwrap, hashlib
root=Path("gitlab-ch33-checkpoint")
orig=root/"original"; work=root/"working"
orig.mkdir(parents=True,exist_ok=True); work.mkdir(parents=True,exist_ok=True)
(orig/"context.json").write_text(json.dumps({
"session_id":"SESSION_FAKE_CAPSTONE_33",
"project":"PROJECT_X",
"actor":"USER_A",
"task":"Fix synthetic path traversal in template resolver",
"data_classes":["synthetic_source","synthetic_vulnerability"],
"prohibited_data":["real_secrets","customer_data","production_logs","private_personal_data"],
"offering_baseline":"GitLab.com fixture",
"version_baseline":"19.3",
"credits_consumed":0,
"live_ai_called":False
},indent=2))
(orig/"policy.json").write_text(json.dumps({
"read_repository":"always_allow",
"read_vulnerability":"always_allow",
"write_feature_branch":"always_ask",
"create_merge_request":"always_ask",
"edit_ci_policy":"always_deny",
"delete_project":"always_deny",
"external_mcp":"always_deny"
},indent=2))
(orig/"requests.json").write_text(json.dumps([
{"tool":"read_repository","intent":"inspect resolver"},
{"tool":"read_vulnerability","intent":"inspect synthetic finding"},
{"tool":"write_feature_branch","intent":"apply candidate patch"},
{"tool":"edit_ci_policy","intent":"disable failing security job"},
{"tool":"delete_project","intent":"reset after test failure"}
],indent=2))
safe='''from pathlib import Path\n\ndef template_path(base: Path, name: str) -> Path:\n base=base.resolve()\n candidate=(base/name).resolve()\n try:\n candidate.relative_to(base)\n except ValueError as exc:\n raise ValueError("outside template root") from exc\n return candidate\n'''
unsafe='''from pathlib import Path\n\ndef template_path(base: Path, name: str) -> Path:\n base=base.resolve()\n candidate=(base/name).resolve()\n if not str(candidate).startswith(str(base)):\n raise ValueError("outside template root")\n return candidate\n'''
(orig/"proposal_A.py").write_text(safe)
(orig/"proposal_B.py").write_text(unsafe)
for p in orig.iterdir():
if p.is_file(): (work/p.name).write_bytes(p.read_bytes())
manifest={p.name:hashlib.sha256(p.read_bytes()).hexdigest() for p in orig.iterdir() if p.is_file()}
(root/"original_sha256.json").write_text(json.dumps(manifest,indent=2))
print(root.resolve())
3. Predict governance outcomes before running the evaluator
Write your prediction first. The task requires reading synthetic source/finding, so those reads may proceed. Applying a patch is a repository mutation and should pause for human approval. Disabling a security job is not needed to fix the vulnerability and weakens verification, so deny it. Project deletion is unrelated and destructive, so deny it.
{
"read_repository": "ALLOW",
"read_vulnerability": "ALLOW",
"write_feature_branch": "ASK",
"edit_ci_policy": "DENY",
"delete_project": "DENY"
}
Your prediction is part of the evidence. If the evaluator later produces a different result, investigate the policy rather than silently changing your answer to match the tool.
4. Evaluate the tool policy and record the denied actions
from pathlib import Path
import json
root=Path("gitlab-ch33-checkpoint")/"working"
policy=json.loads((root/"policy.json").read_text())
requests=json.loads((root/"requests.json").read_text())
map_decision={"always_allow":"ALLOW","always_ask":"ASK","always_deny":"DENY"}
results=[]
for r in requests:
mode=policy.get(r["tool"],"always_deny")
row={**r,"mode":mode,"decision":map_decision[mode]}
results.append(row)
print(f"{r['tool']:24} {row['decision']:5} {r['intent']}")
(root/"tool_decisions.json").write_text(json.dumps(results,indent=2))
Expected: the two reads are ALLOW, feature-branch write
is ASK, and CI-policy/project deletion are
DENY. Explain why the denied CI-policy action is
security-relevant: an agent must not “fix” a remediation by removing
the evidence gate that detects failure.
5. Independently test both proposals
from pathlib import Path
import tempfile, importlib.util, json
root=Path("gitlab-ch33-checkpoint")/"working"
def load(path):
spec=importlib.util.spec_from_file_location(path.stem,path)
mod=importlib.util.module_from_spec(spec); spec.loader.exec_module(mod); return mod
def run(path):
m=load(path)
evidence=[]
with tempfile.TemporaryDirectory() as td:
r=Path(td); base=r/"templates"; base.mkdir(); (r/"templates-backup").mkdir()
expected=(base/"ok.html").resolve()
evidence.append(["valid_path",m.template_path(base,"ok.html")==expected])
for label,bad in [("parent_escape","../secret.txt"),("sibling_prefix","../templates-backup/secret.txt")]:
try: m.template_path(base,bad)
except ValueError: evidence.append([label,True])
else: evidence.append([label,False])
return evidence
report={}
for name in ["proposal_A.py","proposal_B.py"]:
ev=run(root/name); passed=all(v for _,v in ev)
report[name]={"passed":passed,"checks":ev}
print(name,"PASS" if passed else "FAIL",ev)
(root/"verification.json").write_text(json.dumps(report,indent=2))
The expected result is Proposal A PASS and Proposal B FAIL on
sibling_prefix. This is the required unsafe
hallucinated/over-broad remediation rejection: Proposal B has a
plausible narrative but fails the security invariant.
6. Conduct the human security review
Tests are necessary but not sufficient. Review Proposal A for scope: it changes only path-containment logic, adds no dependency, sends no network data, does not change CI/security policy, and requires no broader role. For a real project, add full regression CI and scanner verification before merge.
| Review dimension | Proposal A | Proposal B |
|---|---|---|
| Security invariant | Structural ancestry check | String-prefix approximation |
| Focused tests | Pass | Fails sibling-prefix escape |
| Unrelated permissions/settings | None | None in fixture |
| Decision | Candidate for approval | Reject |
| Reason | Evidence supports containment | Counterexample disproves containment |
7. Produce the approval and audit record
from pathlib import Path
import hashlib, json, datetime
root=Path("gitlab-ch33-checkpoint")/"working"
verification=json.loads((root/"verification.json").read_text())
tools=json.loads((root/"tool_decisions.json").read_text())
chosen=root/"proposal_A.py"
record={
"session_id":"SESSION_FAKE_CAPSTONE_33",
"time":"2026-08-22T02:30:00Z",
"reviewer":"REVIEWER_A",
"data_classification":"synthetic-only",
"model_provider":"FIXTURE_NONE",
"live_ai_called":False,
"gitlab_credits_consumed":0,
"proposal_sha256":hashlib.sha256(chosen.read_bytes()).hexdigest(),
"verification":verification,
"tool_decisions":tools,
"approved":"proposal_A.py",
"approved_scope":"simulate write to disposable feature branch only",
"rejected":"proposal_B.py",
"denied_actions":["edit_ci_policy","delete_project","external_mcp"],
"next_required_real_controls":["MR diff review","full CI","security scan","protected branch approvals"]
}
(root/"approval_audit_record.json").write_text(json.dumps(record,indent=2))
print(json.dumps(record,indent=2))
The record is intentionally explicit that no live AI was called and zero credits were consumed. Never represent fixture evidence as a real GitLab AI audit event. In a real eligible environment, link the product’s session/audit identifiers while preserving the same decision fields.
8. Predict and verify the only approved state change
Before simulating the write, predict two things:
- The approved candidate file will appear only in a disposable feature-branch directory; the original fixture remains unchanged.
- No CI-policy or project-deletion artifact will be created because those tools are denied.
from pathlib import Path
import shutil, hashlib, json
root=Path("gitlab-ch33-checkpoint")
orig=root/"original"; work=root/"working"; branch=root/"feature-branch-sim"
branch.mkdir(exist_ok=True)
shutil.copy2(work/"proposal_A.py",branch/"template_loader.py")
assert not (branch/"ci_policy_disabled.flag").exists()
assert not (root/"project_deleted.flag").exists()
manifest=json.loads((root/"original_sha256.json").read_text())
for name,expected in manifest.items():
actual=hashlib.sha256((orig/name).read_bytes()).hexdigest()
assert actual==expected, f"original changed: {name}"
print("VERIFIED: approved branch simulation created; originals preserved; denied effects absent")
9. Review the audit record for privacy and completeness
Check that the record contains stable synthetic identifiers, proposal hash, tool decisions, approval scope, rejected proposal, and next verification gates. Check that it does not contain a real token, email address, private repository path, customer text, vulnerability payload, production log, or live prompt.
from pathlib import Path
import re
p=Path("gitlab-ch33-checkpoint/working/approval_audit_record.json")
text=p.read_text()
patterns={
"gitlab_token": r"g[l]pat-[A-Za-z0-9_-]{10,}",
"private_key": r"BEGIN (?:RSA |OPENSSH )?PRIVATE KEY",
"email": r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}"
}
for name,pat in patterns.items():
print(name,"FOUND" if re.search(pat,text) else "not found")
A simple regex is not a complete data-loss-prevention system. It is a checkpoint to reinforce the habit: auditability should not become a reason to duplicate sensitive content.
10. Production handoff into Chapter 34
Your Chapter 33 output can be summarized as a platform control contract:
AI GOVERNANCE HANDOFF
Scope: PROJECT_X synthetic remediation
Data: synthetic source + synthetic finding only
Product assumptions: GitLab 19.3 reference; fixture path; no live AI/credits
Authority:
reads -> allowed for approved data
feature-branch write -> human approval
CI policy / delete / external MCP -> denied
Verification:
Proposal A passes containment tests
Proposal B rejected by sibling-prefix counterexample
real deployment would still require MR, full CI, scanners, protected rules
Audit:
proposal hash + reviewer + decisions + denied actions recorded
Fallback:
core Git/CI/security workflow functions without GitLab Duo
Risk owner: platform/security governance
Chapter 34 will combine this AI control contract with the rest of the course: namespaces, access, merge governance, CI/CD, runners, releases, registries, security policies, Kubernetes integration, APIs, backup/recovery, and operational evidence.
11. Cleanup / rollback
Review paths before deletion. The checkpoint is disposable, but the same pattern in production would follow evidence-retention and change-management policy rather than immediate deletion.
from pathlib import Path
import shutil
p=Path("gitlab-ch33-checkpoint")
print("REVIEW BEFORE DELETE:",p.resolve())
# shutil.rmtree(p) # Uncomment only after confirming this is the disposable fixture.
Knowledge check
Why is Proposal B rejected even though its implementation sounds like it checks the same condition?
The sibling-prefix counterexample proves string-prefix equality is not filesystem containment. Reproducible evidence overrides plausible wording.
What two state changes must be predicted before the simulated write?
Only the approved feature-branch file should be created, while original evidence remains unchanged and denied CI-policy/project-delete effects remain absent.
Why does the approval record include the proposal hash?
It binds approval to the exact reviewed bytes so a later different proposal cannot be represented as the approved one.
What does “zero credits consumed” mean in this checkpoint?
Only that the mandatory workflow is a local fixture with no live AI call. It makes no claim about the cost of an equivalent real Agent Platform session.
Which controls remain mandatory if a real AI-generated MR is created?
Normal MR diff review, full CI/tests, relevant security scans, protected branch/approval rules, least-privilege identity, and human authorization remain authoritative.
12. Lesson summary and bridge
- You classified data and tool authority before executing any simulated action.
- You required human approval for the reversible write and denied unrelated security-policy and destructive actions.
- Independent tests rejected a plausible but unsafe AI remediation and supported the safer candidate.
- Your approval record binds exact proposal identity, reviewer, evidence, scope, denied actions, privacy assumptions, and zero-live-credit fixture status.
- Core delivery remains independent of AI availability: Git, CI, scanners, policies, and human review stay authoritative.
Chapter 33 adds governed AI assistance to the production GitLab operating model. Chapter 34 — Production Capstone: Build and Govern a Secure GitLab Software Delivery Platform — now integrates every major course boundary into one end-to-end delivery platform.
Primary sources and version notes
These lessons were finalized against current official GitLab documentation on 2026-08-22 with GitLab 19.3 as the release baseline. GitLab Duo and the Agent Platform are unusually version-volatile: feature maturity, model selection, credits, availability, governance behavior, UI placement, and data-processing details can change between releases. Re-check the documentation for the exact offering, subscription, add-on, and GitLab version you use. The checkpoint intentionally uses a local synthetic record rather than claiming to reproduce the GitLab AI audit-event report schema. For live Agent Platform work, use the current product-generated session/audit artifacts and your organization’s evidence-retention requirements.
- GitLab 19.3 release
- GitLab Duo Agent Platform
- GitLab Duo non-agentic feature summary
- GitLab Credits and usage billing
- GitLab Duo data usage and privacy
- Control GitLab Duo availability
- Control Agent Platform availability
- Agents
- Flows
- Foundational flows
- Configure flow execution
- Agent tool governance
- Audit AI events
- GitLab Duo prompt guardrails
- GitLab Duo contextual awareness
- GitLab Duo Chat
- Explain vulnerabilities with AI
- Resolve vulnerabilities with AI
- Troubleshoot the Agent Platform
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.