fix(web): reject drafts whose digits or types would not survive being sent

Copilot's seventh pass, three comments, all the same class: the editor showing
one value while the wire carries another.

- `JSON.parse` rounds a whole number past 2^53−1 to the nearest double, so a
  draft reading `{"id":9007199254740993}` submits `…992`. New
  `hasRoundedInteger`, applied by all four JSON-draft parsers — the raw
  arguments editor, `SchemaJsonField`, the Edit-and-replay modal, and
  `parseJsonObjectDraft`, which Copilot did not flag but shares the defect.

  It is narrower than "unsafe integer": it also requires the number's shortest
  form to be written out in full, because JS stringifies from 1e21 upward in
  exponent form and such a value was typed that way — `{"n":1e308}` is a
  legitimate `_meta` value that `parseJsonObjectDraft`'s tests already pin as
  accepted. It still refuses the rare full-form integer that happens to be
  exactly representable, erring toward refusing a sendable value rather than
  misreporting an unsendable one — the trade `toNumericValue` already makes.

- `getPrompt` runs `convertPromptArguments`, which JSON-stringifies every
  non-string argument, so `{"count":2}` leaves as `{"count":"2"}` while the
  editor still shows a number. `reshapedReplayParam` now rejects a non-string
  prompt argument. Not a workaround: the spec types
  `GetPromptRequest.params.arguments` as `Record<string, string>`, so a string
  is the only thing a prompt argument can be. `tools/call` is untouched —
  `callTool` sends its arguments as given.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UL9retfvAXRgvi6EWk4SY4
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-27 04:34:45 -04:00
commit 8f4aa7f8af

View file

@ -1,7 +1,11 @@
import { useState } from "react";
import { Alert, Button, Group, Modal, Stack, Text } from "@mantine/core";
import { JsonEditor } from "../../elements/JsonEditor/JsonEditor";
import { isJsonObject } from "../../../utils/jsonObjectDraft";
import {
hasRoundedInteger,
isJsonObject,
ROUNDED_INTEGER_ERROR,
} from "../../../utils/jsonObjectDraft";
import {
replayableParams,
reshapedReplayParam,
@ -95,6 +99,9 @@ function parseParamsDraft(
"Numbers must be finite — a value like `1e400` overflows and would be sent as null",
};
}
if (hasRoundedInteger(parsed)) {
return { ok: false, error: ROUNDED_INTEGER_ERROR };
}
// Seeding from the projection is only half of it: a key the dispatcher does
// not read can also be *added* to the draft, and Send would then discard an
// edit the user was looking at. Checked with the same function the seed is