Skip to content Documentation index for agents (llms.txt)
Glotto Beta
Get started

Drift detection

Glotto commits generated code to Git and guards it with drift detection. The check regenerates your SDKs/docs/MCP server in memory and fails the PR if the committed output no longer matches what the generator produces from the current spec + glotto.yml.

Why commit generated code

  • No slow-install problem — consumers pull ready-to-use code; there’s no toolchain (JVM, Python, Go) running during their install.
  • Reviewable output — code review reads the generator’s actual output, not an inference of it. A generator change shows up as a concrete diff.

How the gate works

drift detection regenerates and diffs against the committed tree; on drift it exits non-zero and reports the diff. Glotto emits a per-VCS-provider CI workflow that gates every PR with verification report — the check fails on drift or a hand-edited managed file, and the verification report (pinned spec/config hashes, per-target drift and custom-code integrity) is published to the job summary and CI artifacts — so “someone changed the generator but forgot to regenerate” can’t slip through, and the proof is visible on the PR itself.

See drift detection and verification report in the CLI reference for the full flag set — comparing against the directories a workspace file declares (no flag), a local tree (--against) or a remote branch (--provider), and emitting the CI workflow (--emit-workflow).

What the gate actually prints

Nothing below is typed by hand. Both panes are what renderDriftReport emitted over the committed fixture this repo dogfoods — the same tree the drift gate regenerates on every one of our own pull requests.

the committed SDK, regenerated and compared — the state every green PR is in

The state every green PR is in: 66 committed files, regenerated and compared.

  • files compared 81
  • out of sync 0
✓ SDK in sync — no drift detected.
regenerated + byte-diffed in CI renderDriftReport (@glotto/core-vcs) packages/cli/tests/fixtures/drift-gate 0410e9dad464

Change one word of the spec and leave the committed SDK alone, and the same check says so — across every target, not just the SDK:

one word changed in the spec — `summary: List pets` became `summary: List every pet`

One summary reworded in the spec. Eleven files out of sync, reported as a diff you can read.

  • files compared 81
  • out of sync 11
## SDK drift detected

11 files out of sync with the committed SDK.

### changed: `cli/README.md`
```diff
- | `pets listPets` | `GET /pets` | List pets |
+ | `pets listPets` | `GET /pets` | List every pet |
```

### changed: `cli/cli.js`
```diff
-     "summary": "List pets",
+     "summary": "List every pet",
```

### changed: `graph/graph.json`
```diff
-         "summary": "List pets"
+         "summary": "List every pet"
```

### changed: `mcp/src/server.ts`
```diff
- // @glotto:generated-checksum e30acd0f04fffbf5e8122b85c3c76c9dbe50cece7ff42c7e47803ef9227fb641
- // Generated by @glotto/codegen-mcp. Edit glotto.yml, not this file.
- import { McpServer } from '@modelcontextprotocol/server';
- import { z } from 'zod';
- import { type ClientCapabilities, jsonCoercer, nameAdapter, parseCapabilities, type SchemaAdapter, schemaAdapter } from './adapt.js';
- import { registerDynamicTools, registerOperationTools } from './dynamic.js';
- import type { ServerDescription } from './debug.js';
- import { parseFilters, shouldRegister } from './filters.js';
- import { parseMode, type ServerMode } from './mode.js';
- import { browseOperations, type OperationEntry } from './operations.js';
- import { forwardedCredentialSource } from './passthrough.js';
- import { clampText, errorResult, projectText } from './results.js';
- import { createSandbox, type SandboxSdkBinding } from './sandbox.js';
- import { SDK_ENTRYPOINT, SDK_FILES } from './sdk-source.js';
- import { buildMergedIndex, resolveResultModels, searchDocsPage, type SearchRecord, type TypeDescriptor } from './search.js';
- 
- const SERVER_NAME = "petstore-mcp";
- const BASE_URL = process.env['GLOTTO_API_BASE_URL'] ?? "https://api.petstore.example";
- const SANDBOX_PERMISSIONS = { allowNet: ["api.petstore.example"] };
- // glotto.yml#/mcp#/modes: a requested-but-disabled mode resolves to the enabled default.
- const ENABLED_MODES: readonly ServerMode[] = ["tools","code","dynamic"];
- 
- // Named models referenced by the tool schemas; z.lazy so reference cycles resolve.
- const Model_Pet: z.ZodTypeAny = z.lazy(() => z.object({ "id": z.number().int(), "name": z.string() }));
- 
- const SEARCH_INDEX_MODELS: Record<string, TypeDescriptor> = {
-   "Pet": {
-     "kind": "object",
-     "fields": {
-       "id": {
-         "kind": "integer"
-       },
-       "name": {
-         "kind": "string"
-       }
-     },
-     "required": [
-       "id",
-       "name"
-     ]
-   }
- };
- const SEARCH_INDEX_BASELINE: SearchRecord[] = [
-   {
-     "kind": "reference",
-     "title": "Create a pet",
-     "url": "/reference/pets/createPet",
-     "resource": "pets",
-     "method": "createPet",
-     "httpMethod": "POST",
-     "path": "/pets",
-     "tool": "pets_createPet",
-     "sdkCall": "client.pets.createPet",
-     "parameters": [],
-     "text": "Create a pet",
-     "requestBody": {
-       "required": true,
-       "contentType": "application/json",
-       "schema": {
-         "kind": "model",
-         "model": "Pet"
-       }
-     },
-     "response": {
-       "kind": "model",
-       "model": "Pet"
-     },
-     "sampleInput": {
-       "id": 0,
-       "name": "<name>"
-     }
-   },
-   {
-     "kind": "reference",
-     "title": "List pets",
-     "url": "/reference/pets/listPets",
-     "resource": "pets",
-     "method": "listPets",
-     "httpMethod": "GET",
-     "path": "/pets",
-     "tool": "pets_listPets",
-     "sdkCall": "client.pets.listPets",
-     "parameters": [],
-     "text": "List pets",
-     "response": {
-       "kind": "array",
-       "items": {
-         "kind": "model",
-         "model": "Pet"
-       }
-     }
-   }
- ];
- const DOCS_INDEX_LOCATION = process.env['GLOTTO_DOCS_INDEX'];
- // Loaded once at startup; the search_docs handler awaits this cached merged index.
- const docsIndexPromise = buildMergedIndex(SEARCH_INDEX_BASELINE, DOCS_INDEX_LOCATION);
- 
- // Attach the upstream API credential to each outbound Tools Mode request: the caller-forwarded
- // credential when credential passthrough is armed (mcp-credential-passthrough #3350), else this
- // caller's own upstream OAuth token when that flow is on (#3351), else the operator's
- // environment. Code Mode reads this too (#3348): its guest↔SDK binding resolves the
- // credential HERE, per request, so the guest's client and the Tools Mode handlers can
- // never disagree about which credential this caller reaches the API with.
- function applyAuth(headers: Record<string, string>): void {
-   const forwarded = forwardedCredentialSource();
-   const token = forwarded !== undefined ? forwarded.authorization : process.env["GLOTTO_API_TOKEN"];
-   if (token) headers['Authorization'] = "Bearer " + token;
- }
- 
- // The shared operation table (mcp-dynamic-tools-mode, ADR-0088), in collectOperations order:
- // Tools Mode registers each entry as its own tool; Dynamic Mode serves the same entries
- // through the list_tools/describe_tools/invoke_tool meta-tools.
- const OPERATIONS: readonly OperationEntry[] = [
-   {
-     name: "pets_createPet",
-     description: "Create a pet",
-     resource: "pets",
-     method: "createPet",
-     httpMethod: "POST",
-     path: "/pets",
-     meta: { resource: "pets", tags: ["pets"] },
-     jqInjected: true,
-     inputShape: (coerce) => ({ body: coerce(Model_Pet), jq_filter: z.string().describe("jq-style filter to shape the text result (subset: .field, .[\"key\"], .[0], .[] iteration, | pipes); applied to the JSON response body").optional() }),
-     outputSchema: z.object({ "id": z.number().int(), "name": z.string() }),
-     handler: async (input, jqFilter) => {
-       const path = "/pets";
-       const url = new URL(BASE_URL + path);
-       const headers: Record<string, string> = {};
-       headers['content-type'] = 'application/json';
-       applyAuth(headers);
-       const response = await fetch(url, {
-         method: "POST",
-         headers,
-         body: JSON.stringify(input['body']),
-       });
-       const text = await response.text();
-       if (!response.ok) return errorResult(response.status, text);
-       let structured: unknown;
-       try {
-         structured = JSON.parse(text);
-       } catch {
-         structured = undefined;
-       }
-       if (typeof structured !== 'object' || structured === null || Array.isArray(structured)) {
-         return { content: [{ type: 'text', text: 'HTTP ' + response.status + ': expected a JSON object response body but received: ' + (text === '' ? 'an empty body' : text) }], isError: true };
-       }
-       const projected = projectText(text, jqFilter);
-       if (!projected.ok) return { content: [{ type: 'text', text: projected.text }], isError: true };
-       return { content: [{ type: 'text', text: clampText(projected.text) }], structuredContent: structured as Record<string, unknown> };
-     },
-   },
-   {
-     name: "pets_listPets",
-     description: "List pets",
+ // @glotto:generated-checksum 53232a1a568b12e4d6b606365ec19a3c1375664757cc1611e9edecef1a193b61
+ // Generated by @glotto/codegen-mcp. Edit glotto.yml, not this file.
+ import { McpServer } from '@modelcontextprotocol/server';
+ import { z } from 'zod';
+ import { type ClientCapabilities, jsonCoercer, nameAdapter, parseCapabilities, type SchemaAdapter, schemaAdapter } from './adapt.js';
+ import { registerDynamicTools, registerOperationTools } from './dynamic.js';
+ import type { ServerDescription } from './debug.js';
+ import { parseFilters, shouldRegister } from './filters.js';
+ import { parseMode, type ServerMode } from './mode.js';
+ import { browseOperations, type OperationEntry } from './operations.js';
+ import { forwardedCredentialSource } from './passthrough.js';
+ import { clampText, errorResult, projectText } from './results.js';
+ import { createSandbox, type SandboxSdkBinding } from './sandbox.js';
+ import { SDK_ENTRYPOINT, SDK_FILES } from './sdk-source.js';
+ import { buildMergedIndex, resolveResultModels, searchDocsPage, type SearchRecord, type TypeDescriptor } from './search.js';
+ 
+ const SERVER_NAME = "petstore-mcp";
+ const BASE_URL = process.env['GLOTTO_API_BASE_URL'] ?? "https://api.petstore.example";
+ const SANDBOX_PERMISSIONS = { allowNet: ["api.petstore.example"] };
+ // glotto.yml#/mcp#/modes: a requested-but-disabled mode resolves to the enabled default.
+ const ENABLED_MODES: readonly ServerMode[] = ["tools","code","dynamic"];
+ 
+ // Named models referenced by the tool schemas; z.lazy so reference cycles resolve.
+ const Model_Pet: z.ZodTypeAny = z.lazy(() => z.object({ "id": z.number().int(), "name": z.string() }));
+ 
+ const SEARCH_INDEX_MODELS: Record<string, TypeDescriptor> = {
+   "Pet": {
+     "kind": "object",
+     "fields": {
+       "id": {
+         "kind": "integer"
+       },
+       "name": {
+         "kind": "string"
+       }
+     },
+     "required": [
+       "id",
+       "name"
+     ]
+   }
+ };
+ const SEARCH_INDEX_BASELINE: SearchRecord[] = [
+   {
+     "kind": "reference",
+     "title": "Create a pet",
+     "url": "/reference/pets/createPet",
+     "resource": "pets",
+     "method": "createPet",
+     "httpMethod": "POST",
+     "path": "/pets",
+     "tool": "pets_createPet",
+     "sdkCall": "client.pets.createPet",
+     "parameters": [],
+     "text": "Create a pet",
+     "requestBody": {
+       "required": true,
+       "contentType": "application/json",
+       "schema": {
+         "kind": "model",
+         "model": "Pet"
+       }
+     },
+     "response": {
+       "kind": "model",
+       "model": "Pet"
+     },
+     "sampleInput": {
+       "id": 0,
+       "name": "<name>"
+     }
+   },
+   {
+     "kind": "reference",
+     "title": "List every pet",
+     "url": "/reference/pets/listPets",
+     "resource": "pets",
+     "method": "listPets",
+     "httpMethod": "GET",
+     "path": "/pets",
+     "tool": "pets_listPets",
+     "sdkCall": "client.pets.listPets",
+     "parameters": [],
+     "text": "List every pet",
+     "response": {
+       "kind": "array",
+       "items": {
+         "kind": "model",
+         "model": "Pet"
+       }
+     }
+   }
+ ];
+ const DOCS_INDEX_LOCATION = process.env['GLOTTO_DOCS_INDEX'];
+ // Loaded once at startup; the search_docs handler awaits this cached merged index.
+ const docsIndexPromise = buildMergedIndex(SEARCH_INDEX_BASELINE, DOCS_INDEX_LOCATION);
+ 
+ // Attach the upstream API credential to each outbound Tools Mode request: the caller-forwarded
+ // credential when credential passthrough is armed (mcp-credential-passthrough #3350), else this
+ // caller's own upstream OAuth token when that flow is on (#3351), else the operator's
+ // environment. Code Mode reads this too (#3348): its guest↔SDK binding resolves the
+ // credential HERE, per request, so the guest's client and the Tools Mode handlers can
+ // never disagree about which credential this caller reaches the API with.
+ function applyAuth(headers: Record<string, string>): void {
+   const forwarded = forwardedCredentialSource();
+   const token = forwarded !== undefined ? forwarded.authorization : process.env["GLOTTO_API_TOKEN"];
+   if (token) headers['Authorization'] = "Bearer " + token;
+ }
+ 
+ // The shared operation table (mcp-dynamic-tools-mode, ADR-0088), in collectOperations order:
+ // Tools Mode registers each entry as its own tool; Dynamic Mode serves the same entries
+ // through the list_tools/describe_tools/invoke_tool meta-tools.
+ const OPERATIONS: readonly OperationEntry[] = [
+   {
+     name: "pets_createPet",
+     description: "Create a pet",
+     resource: "pets",
+     method: "createPet",
+     httpMethod: "POST",
+     path: "/pets",
+     meta: { resource: "pets", tags: ["pets"] },
+     jqInjected: true,
+     inputShape: (coerce) => ({ body: coerce(Model_Pet), jq_filter: z.string().describe("jq-style filter to shape the text result (subset: .field, .[\"key\"], .[0], .[] iteration, | pipes); applied to the JSON response body").optional() }),
+     outputSchema: z.object({ "id": z.number().int(), "name": z.string() }),
+     handler: async (input, jqFilter) => {
+       const path = "/pets";
+       const url = new URL(BASE_URL + path);
+       const headers: Record<string, string> = {};
+       headers['content-type'] = 'application/json';
+       applyAuth(headers);
+       const response = await fetch(url, {
+         method: "POST",
+         headers,
+         body: JSON.stringify(input['body']),
+       });
+       const text = await response.text();
+       if (!response.ok) return errorResult(response.status, text);
+       let structured: unknown;
+       try {
+         structured = JSON.parse(text);
+       } catch {
+         structured = undefined;
+       }
+       if (typeof structured !== 'object' || structured === null || Array.isArray(structured)) {
+         return { content: [{ type: 'text', text: 'HTTP ' + response.status + ': expected a JSON object response body but received: ' + (text === '' ? 'an empty body' : text) }], isError: true };
+       }
+       const projected = projectText(text, jqFilter);
+       if (!projected.ok) return { content: [{ type: 'text', text: projected.text }], isError: true };
+       return { content: [{ type: 'text', text: clampText(projected.text) }], structuredContent: structured as Record<string, unknown> };
+     },
+   },
+   {
+     name: "pets_listPets",
+     description: "List every pet",
```

### changed: `openapi.decorated.json`
```diff
-         "summary": "List pets",
+         "summary": "List every pet",
```

### changed: `spec_repo/spec.base.json`
```diff
-         "summary": "List pets",
+         "summary": "List every pet",
```

### changed: `spec_repo/spec.base.yaml`
```diff
-       summary: List pets
+       summary: List every pet
```

### changed: `spec_repo/spec.with-code-samples.json`
```diff
-         "summary": "List pets",
+         "summary": "List every pet",
```

### changed: `spec_repo/spec.with-code-samples.yaml`
```diff
-       summary: List pets
+       summary: List every pet
```

### changed: `spec_repo/spec.with-transforms.json`
```diff
-         "summary": "List pets",
+         "summary": "List every pet",
```

### changed: `spec_repo/spec.with-transforms.yaml`
```diff
-       summary: List pets
+       summary: List every pet
```
regenerated + byte-diffed in CI renderDriftReport (@glotto/core-vcs) packages/cli/tests/fixtures/drift-gate 0560947c783e

This only works because the pipeline is deterministic — a byte-stable canonical spec means regeneration is reproducible, so a diff means a real change, not noise.