A documentation pipeline can draft HTTP call recipes from recorded fixtures without inventing a single credential-lifecycle sentence. The model may reshape method, path, headers, and body examples that already exist in a reviewed trace file. A human must still own every instruction that explains how readers obtain, store, rotate, or revoke secrets. Mixing those two lanes produces plausible onboarding copy that no OpenAPI document or fixture can verify.
This article describes a split that is narrower than a general “AI writes the docs” policy. Generated Markdown may describe one recorded call, including redacted headers that already appear in the fixture. Generated Markdown may not teach readers how to mint keys, place tokens on disk, refresh sessions, or file a rotation ticket. The compiler, the scanner, and the human-owned auth page are the three artifacts that keep those lanes apart.
What the model may draft versus what a human must own
The split is not about writing quality. It is about evidence that already exists in a reviewed file versus evidence that never appears in OpenAPI or HTTP traces. A fixture can prove that GET /v1/widgets/{id} returned 200 with a particular JSON shape on a given commit. A fixture cannot prove how your company issues tokens, how long those tokens remain valid, or which secret store operators should use.
| Lane | Source of truth | Model may draft | Human must own |
|---|---|---|---|
| Call recipe | Redacted HTTP fixture plus the matching OpenAPI operation | Method, path, echoed headers, request body, expected status, response excerpt | Whether the recipe is current for this release |
| Schema narration | OpenAPI schema objects already reviewed |
Field names and types that the schema lists | Product meaning of names such as tenant or workspace |
| Credential lifecycle | Internal runbooks, IdP config, legal review | Nothing | How to obtain, store, rotate, revoke, and report leaked credentials |
| Multi-step orchestration | Incident history and on-call practice | Nothing that sequences login, consent, or refresh | Order of operations, failure handling, and abort conditions |
Treat the first two rows as compile input. Treat the last two rows as a separate repository path that generation jobs cannot write.
A fixture the compiler is allowed to see
Record one successful call, then redact secrets before the file enters the docs repository. The fixture below is a worked example, not a production capture from a named service.
{
"operation_id": "getWidget",
"recorded_at_commit": "REPLACE_WITH_REVIEWED_SHA",
"request": {
"method": "GET",
"path": "/v1/widgets/wg_123",
"headers": {
"accept": "application/json",
"authorization": "Bearer REDACTED"
}
},
"response": {
"status": 200,
"headers": {
"content-type": "application/json"
},
"body": {
"id": "wg_123",
"name": "demo-widget",
"status": "active"
}
}
}
Store fixtures under fixtures/http/ and keep a sibling OpenAPI fragment that names the same operation_id. The compiler should refuse to run when the path, method, or status in the fixture disagrees with the spec. That refusal is cheaper than a later docs incident in which a generated recipe teaches a retired route.
Numbered workflow
Follow the eight steps in order. Skipping redaction or the scanner reintroduces the failure mode this pipeline exists to prevent.
- Capture one HTTP exchange against a known commit and write it as JSON beside the OpenAPI file.
- Redact tokens, cookies, internal hostnames, and tenant identifiers before the fixture is committed.
- Confirm that
operation_id, method, and path match the reviewed specification for that commit. - Hand the redacted fixture and the single operation schema to a drafting model as read-only context.
- Require the model to emit Markdown that only restates fields present in those two files.
- Run a scanner that fails the build when generated files contain credential-lifecycle instruction patterns.
- Merge generated recipes into
docs/recipes/generated/and keep auth onboarding indocs/auth/HUMAN_OWNED.md. - Review the human-owned file whenever IdP settings, token lifetime, or secret-storage policy change.
If a team already uses MonkeyCode, the free model access and the free server option can host this compile job without turning the model into an identity provider. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The drafting step should receive only redacted fixtures and the matching operation, then emit recipe Markdown that the scanner will reject when credential-lifecycle prose appears.
Compiler sketch
The Python below is a local example for one operation. Label it unexecuted until you point SPEC and FIXTURE at files you actually maintain.
# compile_recipe.py — example, not a published package
import json
import pathlib
import sys
import yaml
SPEC = pathlib.Path("openapi.yaml")
FIXTURE = pathlib.Path("fixtures/http/get-widget.json")
OUT = pathlib.Path("docs/recipes/generated/get-widget.md")
ALLOWED_HEADER_KEYS = {"accept", "content-type", "authorization"}
def load_operation(spec, operation_id):
for path, methods in spec.get("paths", {}).items():
for method, op in methods.items():
if isinstance(op, dict) and op.get("operationId") == operation_id:
return method.upper(), path, op
raise SystemExit(f"operation {operation_id} missing from spec")
def main():
spec = yaml.safe_load(SPEC.read_text())
fx = json.loads(FIXTURE.read_text())
method, path_template, op = load_operation(spec, fx["operation_id"])
if fx["request"]["method"].upper() != method:
raise SystemExit("fixture method does not match OpenAPI")
echoed = {
k: v for k, v in fx["request"]["headers"].items()
if k.lower() in ALLOWED_HEADER_KEYS
}
lines = [
f"# Recipe: {fx['operation_id']}",
"",
f"Recorded against commit `{fx['recorded_at_commit']}`.",
"This page restates one redacted fixture. It does not describe credential lifecycle.",
"",
f"- Method: `{method}`",
f"- Path template: `{path_template}`",
f"- Recorded path: `{fx['request']['path']}`",
f"- Expected status: `{fx['response']['status']}`",
"",
"## Headers present in the fixture",
"",
]
for key, value in echoed.items():
lines.append(f"- `{key}`: `{value}`")
lines.extend([
"",
"## Response excerpt from the fixture",
"",
"```
json",
json.dumps(fx["response"]["body"], indent=2),
"
```",
"",
])
OUT.parent.mkdir(parents=True, exist_ok=True)
OUT.write_text("\n".join(lines))
if __name__ == "__main__":
main()
Run it only after the fixture and spec agree:
python3 compile_recipe.py
git diff -- docs/recipes/generated/get-widget.md
The generated page may echo Authorization: Bearer REDACTED because that header exists in the fixture. It still must not explain where the bearer token came from, how long it lasts, or where operators should store the replacement.
Scanner that keeps credential copy out of generated files
Regex is not semantic understanding, but a small denylist catches the instructional sentences models invent most often. Keep the denylist in version control next to the compiler so reviewers can see which phrases are banned in generated lanes.
# assert_no_credential_copy.py — example scanner
import pathlib
import re
import sys
ROOT = pathlib.Path("docs/recipes/generated")
PATTERNS = [
r"create an api key",
r"generate a token",
r"store (it|the token|the key) in",
r"put this in your \\.env",
r"never commit",
r"rotate (the|your) (key|token|secret)",
r"refresh token",
r"oauth consent",
r"expires in \\d+",
r"keep this secret",
r"paste your credentials",
]
def main():
failures = []
for path in ROOT.rglob("*.md"):
text = path.read_text().lower()
for pat in PATTERNS:
if re.search(pat, text):
failures.append(f"{path}: matched /{pat}/")
if failures:
print("credential-lifecycle copy found in generated recipes:")
print("\n".join(failures))
sys.exit(1)
if __name__ == "__main__":
main()
Wire both tools into the same job so a green compile cannot ship without a green scan:
python3 compile_recipe.py
python3 assert_no_credential_copy.py
If the scanner fails, do not patch the generated file by hand. Either the model added lifecycle advice and the prompt must shrink, or a fixture leaked instructional commentary and the capture process must change. Hand edits in docs/recipes/generated/ become unreviewable within a few regenerations.
Human-owned credential file
Place lifecycle instructions in a path the compiler cannot write. The stub below shows ownership markers, not a complete security program.
<!-- docs/auth/HUMAN_OWNED.md -->
# Credential lifecycle (human-owned)
Status: owned by the API platform group.
Last reviewed against commit: REPLACE_WITH_REVIEWED_SHA.
This file is the only docs path allowed to explain how readers obtain,
store, rotate, or revoke credentials. Generated recipes must link here
instead of restating these steps.
1. Issue credentials through the internal access request path your security team names.
2. Store secrets in the vault product your operator runbook names, not in recipe files.
3. Rotate on the interval the identity provider currently enforces.
4. Revoke and report through the incident channel listed in the on-call wiki.
Generated recipes can include a single relative link to that file. They should not paraphrase it. Paraphrase is how token lifetimes and storage advice drift out of date while the fixture-backed status code remains correct.
Failure analysis the scanner will not catch
Three residual failures remain after the denylist is in place. First, a fixture that was redacted poorly still echoes a live token inside an allowed header key, and the compiler will faithfully reprint it. Second, a model can avoid banned phrases and still invent a plausible host or query parameter that the fixture never contained, unless you also diff emitted values against the fixture map. Third, a multi-step product flow such as device authorization cannot be represented as one recorded call, so generating a walkthrough from a single fixture will omit required human sequencing.
Add a value-diff after generation if your recipes include bodies or query strings. The check can be strict equality on keys that originated in the fixture, plus a failure when the Markdown introduces keys the fixture and schema both lack. That second assertion is the practical companion to the phrase scanner.
# excerpt: reject keys that neither fixture nor schema listed
allowed_keys = set(fx["response"]["body"].keys())
emitted_keys = set(parsed_response_excerpt.keys())
unexpected = emitted_keys - allowed_keys
if unexpected:
raise SystemExit(f"generated keys not in fixture: {sorted(unexpected)}")
Limitations
This workflow assumes a reviewed OpenAPI file and at least one redacted fixture per published recipe. Teams without those inputs are generating fiction, not compiling examples. The denylist will miss paraphrases, especially in languages other than English, and it will not detect a screenshot or a fenced diagram that contains a secret. Fixtures also age: a 200 recorded last quarter does not prove the operation still behaves that way after a serializer change.
The free model and free server path is only a place to run the drafting job. It does not certify that a recipe is accurate, that redaction succeeded, or that the human-owned auth page is complete. Do not treat a successful compile as a security review.
Who should not use this approach
Do not use this split if your public documentation must legally include credential issuance on the same page as the HTTP example and you lack a merge step that inserts a human-reviewed excerpt. Do not use it for APIs whose example URLs or tenant shapes are themselves restricted data. Do not use it as a substitute for an identity-provider runbook, a secret-scanner in CI, or a process for reporting leaked tokens. Authors who want a model to invent OAuth bootstrap, cookie flags, or key-rotation calendars are asking the wrong lane to do security writing.
The useful outcome is smaller and more testable than a full documentation rewrite. One operation, one redacted fixture, one generated recipe, and one human-owned credential page are enough to prove the lanes stay separate. If you trial the scanner on an existing docs repository, start with a single operation and a redacted fixture rather than regenerating the entire reference set.
Top comments (0)