Without governance, AI agents default to reading entire files. This wastes tokens, risks exposing sensitive code regions, and creates unpredictable context costs. SDL-MCP’s policy engine enforces a “prove you need it” model for raw code access.
How It Works
Every request to sdl.code.needWindow (Rung 4 — raw code) passes through the policy engine before code is returned:
Agent Request
"I need to read validateToken()"
│
▼
┌──────────────────────────────┐
│ Policy Engine │
│ │
│ ✓ Identifiers provided? │──── deny if empty
│ ✓ Within line limit? │──── deny if > maxWindowLines
│ ✓ Within token limit? │──── deny if > maxWindowTokens
│ ✓ Identifiers exist? │──── deny if not in range
│ ✓ Symbol in current slice? │──── more likely to approve
│ ✓ Scorer utility check │──── is the code worth reading?
│ │
└──────────┬───────────────────┘
│
┌──────┴──────┐
│ │
APPROVE DENY
│ │
▼ ▼
Return code Return guidance
+ audit log + nextBestAction
Policy Configuration
Configure governance in the policy block of your sdlmcp.config.json:
{
"policy": {
"maxWindowLines": 180,
"maxWindowTokens": 1400,
"requireIdentifiers": true,
"allowBreakGlass": false,
"defaultDenyRaw": true
}
}
| Setting | Default | Description |
|---|
maxWindowLines | 180 | Maximum lines per raw code request. Requests above this are clamped, not rejected. |
maxWindowTokens | 1400 | Maximum tokens per raw code request. Requests above this are clamped. |
requireIdentifiers | true | Agent must specify non-empty identifiersToFind — what it expects to find in the code. |
allowBreakGlass | false | Allow emergency override with full audit logging. |
defaultDenyRaw | true | Default deny for raw code windows — requires proof-of-need (symbol in slice, identifiers provided, reason given). |
budgetCaps | null | Optional server-side defaults: { "maxCards": 60, "maxEstimatedTokens": 12000 }. |
maxWindowLines and maxWindowTokens are clamped, not rejected. An agent requesting 500 lines gets 180 lines, not an error. This prevents agents from failing on policy limits while still bounding token spend.
What Agents Must Provide for Rung 4 Access
A well-formed sdl.code.needWindow request that is likely to be approved:
{
"repoId": "my-repo",
"symbolId": "sha256:abc...",
"reason": "Need to inspect the exact error handling branch for expired tokens",
"identifiersToFind": ["TokenExpiredError", "expiresAt"],
"expectedLines": 60,
"maxTokens": 800,
"sliceContext": {
"taskText": "debug JWT expiration handling",
"stackTrace": "Error: invalid signature\n at validateToken (auth/jwt.ts:47)"
}
}
Pass sliceContext to give the gating engine task context — this significantly improves approval likelihood for justified requests. Include taskText, stackTrace, failingTestPath, or editedFiles as available.
Required fields when requireIdentifiers: true (the default):
identifiersToFind — non-empty list of identifiers the agent expects to find
expectedLines — expected line count (must be ≤ maxWindowLines)
reason — plain-English justification for needing raw code
Denied Request Responses
When a request is denied, the response includes actionable guidance rather than just a rejection:
{
"approved": false,
"whyDenied": ["No identifiers matched in the requested range"],
"nextBestAction": {
"tool": "sdl.code.getHotPath",
"args": {
"symbolId": "sha256:abc...",
"identifiersToFind": ["errorCode", "retryCount"]
},
"rationale": "Hot-path can locate these identifiers without full code access"
}
}
Never ignore nextBestAction, fallbackTools, or fallbackRationale in denied responses. These fields tell you exactly what to try instead — agents that skip them waste tokens re-attempting denied requests.
Common denial reasons and the recommended next action:
| Denial Reason | Next Action |
|---|
| No identifiers provided | Add identifiersToFind and retry |
| Identifiers not found in range | Use sdl.code.getHotPath with those identifiers |
| Exceeds line/token limit | Lower expectedLines / maxTokens to within policy limits |
| Symbol not in current slice | Build a slice with this symbol as an entry point first |
| Scorer utility below threshold | Use sdl.code.getSkeleton instead; the skeleton may be sufficient |
Audit Logging
Every raw code access and every denial is logged with:
- Audit hash — unique identifier for the decision
- Request details — who asked, for what symbol, with what justification
- Decision — approve, deny, or downgrade (to skeleton/hot-path)
- Evidence used — what factors influenced the decision
Audit logs are written to the SDL-MCP server logs and are retained for compliance review. Every allowBreakGlass override is logged with the requesting agent’s stated reason.
Break-Glass Override
Break-glass allows emergency bypass of policy denials. It is disabled by default and should remain disabled in production.
{
"policy": {
"allowBreakGlass": true
}
}
Every break-glass override is logged in the audit trail with the agent’s reason. Enable only in development or when a justified emergency requires it. Do not enable in production or CI environments.
When allowBreakGlass: true, an agent can include "breakGlass": true in its needWindow request to bypass the scorer utility check. Identifier and limit checks still apply.
Policy can be read and updated at runtime without restarting the server.
sdl.policy.get
Read current policy settings:
Response includes all current policy values and their sources (config file vs runtime override).
sdl.policy.set
Update policy at runtime using a merge patch — only supplied fields change:
{
"repoId": "my-repo",
"policyPatch": {
"maxWindowLines": 300,
"requireIdentifiers": true
}
}
Useful policy adjustments:
| Scenario | Patch |
|---|
| Large functions in codebase | { "maxWindowLines": 300, "maxWindowTokens": 2500 } |
| Strict production gating | { "allowBreakGlass": false, "requireIdentifiers": true } |
| CI tighter limits | { "maxWindowLines": 120, "maxWindowTokens": 1000 } |
| Disable identifier requirement | { "requireIdentifiers": false } (not recommended) |
Runtime policy changes via sdl.policy.set are not persisted to the config file. They reset on server restart. To make changes permanent, update sdlmcp.config.json.
Runtime Execution Governance
sdl.runtime.execute has its own separate governance layer:
- Disabled by default — must be explicitly enabled with
runtime.enabled: true in config
- Executable validation — only allowed runtimes and executables can run (configurable allowlist)
- CWD jailing — subprocesses cannot escape the repo root
- Environment scrubbing — only
PATH and explicitly allowlisted env vars are passed through
- Concurrency limits — prevents resource exhaustion (
maxConcurrentJobs: 2 by default)
- Timeout enforcement — hard kill on timeout (
maxDurationMs: 30000 by default)
- Output truncation — stdout and stderr are capped at configurable byte limits
To enable runtime execution:
{
"runtime": {
"enabled": true,
"allowedRuntimes": ["node", "python"],
"maxDurationMs": 30000
}
}