Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 16 additions & 15 deletions src/server/management/native-integration-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,28 +295,29 @@ async function handleCodexToggle(ctx: ManagementContext): Promise<Response> {

async function handleGrokToggle(ctx: ManagementContext): Promise<Response> {
const { req, config, deps } = ctx;
let body: { enabled?: unknown };
try {
body = await readManagementJsonBody(req);
} catch (error) {
rethrowManagementBodyTooLarge(error);
return jsonResponse({ error: "invalid JSON body" }, 400);
}
if (typeof body.enabled !== "boolean") {
return jsonResponse({ error: "enabled must be a boolean" }, 400);
}
const enabled = body.enabled;

/*
* The guard stands BEFORE the first await, or two concurrent PUTs could both
* pass it while their bodies are still parsing — the race the guard exists
* to close.
* Parse the request before taking the mutation flight. A client that stalls
* its upload must not prevent a complete request from changing Grok config.
* The check and assignment remain synchronous, so parsed requests cannot
* race into the mutation section.
*/
if (grokToggleFlight) {
return refusal(409, "grok", "config_busy",
"Another Grok change is already in flight. Nothing was written — try again in a moment.");
}
grokToggleFlight = (async (): Promise<Response> => {
let body: { enabled?: unknown };
try {
body = await readManagementJsonBody(req);
} catch (error) {
rethrowManagementBodyTooLarge(error);
return jsonResponse({ error: "invalid JSON body" }, 400);
}
if (typeof body.enabled !== "boolean") {
return jsonResponse({ error: "enabled must be a boolean" }, 400);
}
const enabled = body.enabled;

/*
* The inspector runs BEFORE either delegate, in BOTH directions (012 §In
* PUT it is the authoritative preflight): an ambiguous fence never reaches
Expand Down
25 changes: 25 additions & 0 deletions tests/native-grok-toggle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,31 @@ test("a second concurrent PUT gets 409 config_busy and writes nothing", async ()
expect(third.status).toBe(200);
});

test("an incomplete request body does not hold the Grok mutation flight", async () => {
writeConfig("# user only\n");
let finishUpload!: () => void;
const body = new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(new TextEncoder().encode('{"enabled":'));
finishUpload = () => controller.close();
},
});
const stalled = dispatch(baseConfig(), "/api/native-integrations/grok", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body,
});
await new Promise(resolve => setTimeout(resolve, 20));

const complete = await put(baseConfig(), true);
expect(complete.status).toBe(200);
expect(complete.body.state).toBe("current");

finishUpload();
const stalledResponse = await stalled;
expect(stalledResponse!.status).toBe(400);
});

test("no journal row and no snapshot exist for this toggle", () => {
// The module must not touch the file-client bookkeeping at all — a static
// check that the imports were never widened (012 §OUT).
Expand Down
Loading