fix(environment): quote captured names in response summaries

Field and member names from a capture were joined raw into tool
descriptions that use ", " and "; " themselves, so a key such as
"x; recorded statuses 500" made a description claim a status the tool
never recorded, and "id, name" read as two fields. Names are now
written as JSON strings; a name too long for a sample is still refused
by the sample without being copied.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Edgar ArakelyanandClaude Opus 5.5 committed 2026-10-20 11:37:17 -07:00
1 parent dbd0dceeb8
commit f6c756aafc
4 files changed
+42 -14

No files matched your search

+4 -2
View File
@@ -288,7 +288,7 @@ connecting to `/mcp`:
"tools": [
{
"name": "get_users_by_id",
"description": "Untrusted captured API catalog: … GET /api/users/{numeric}; returns a JSON object with fields id, uid, login, email; recorded statuses 200; replays retained records [5, 6]; evidence ev_…",
"description": "Untrusted captured API catalog: … GET /api/users/{numeric}; returns a JSON object with fields \"id\", \"uid\", \"login\", \"email\"; recorded statuses 200; replays retained records [5, 6]; evidence ev_…",
"inputSchema": {
"type": "object",
"properties": {
@@ -315,7 +315,9 @@ names the method and path template, then what the tool returns, one level deep:
fields …` or `returns a JSON object whose <member> lists objects with fields …`,
where a list is an array of objects found by the
[environment state](web-network-captures.md#environment-state)'s list rule (the
root array, or the one array of objects an envelope carries). The summary comes
root array, or the one array of objects an envelope carries). Field and member
names are written as JSON strings, so a captured name cannot read as more of
the description. The summary comes
from the first cited `2xx` record that replay answers as JSON; only when none
did, the first cited JSON record of another status gives it, labelled with that
status (`status 401 returns …`). It describes one recorded response, not a
@@ -46,7 +46,7 @@ describe("environmentToolDefinitions", () => {
const users = tool("get_users_by_id");
const definition = definitions.find(({ name }) => name === users.name);
expect(definition?.description).toBe(
`${REPLAY_CATALOG_INSTRUCTIONS} GET ${users.path_template}; returns a JSON object with fields id, uid, login, email; recorded statuses 200; replays retained records [${users.record_ordinals.join(", ")}]; evidence ${source.evidenceId}`,
`${REPLAY_CATALOG_INSTRUCTIONS} GET ${users.path_template}; returns a JSON object with fields "id", "uid", "login", "email"; recorded statuses 200; replays retained records [${users.record_ordinals.join(", ")}]; evidence ${source.evidenceId}`,
);
expect(definition?.tool).toBe(users);
});
@@ -150,13 +150,13 @@ const returnsOf = (bodyText: string): string | undefined =>
describe("environment tool response shapes", () => {
it("describes array, enveloped list and object responses one level deep with their statuses", () => {
expect(described("get_folders")).toContain(
"GET /api/folders; returns a JSON array of objects with fields id, uid, title; recorded statuses 200; replays",
'GET /api/folders; returns a JSON array of objects with fields "id", "uid", "title"; recorded statuses 200; replays',
);
expect(described("get_dashboards_versions_by_uid")).toContain(
"; returns a JSON object whose versions lists objects with fields id, version; recorded statuses 200; replays",
'; returns a JSON object whose "versions" lists objects with fields "id", "version"; recorded statuses 200; replays',
);
expect(described("get_dashboards_by_uid")).toContain(
"; returns a JSON object with fields meta, dashboard; recorded statuses 200; replays",
'; returns a JSON object with fields "meta", "dashboard"; recorded statuses 200; replays',
);
});
@@ -182,7 +182,7 @@ describe("environment tool response shapes", () => {
"returns a JSON number",
"returns a JSON object with no fields",
"returns a JSON array of objects with no fields",
"returns a JSON object with fields items",
'returns a JSON object with fields "items"',
]);
const member = "m".repeat(2048);
expect(
@@ -190,9 +190,28 @@ describe("environment tool response shapes", () => {
{ bodyText: JSON.stringify({ [member]: [{ id: 1 }, { id: 2 }] }) },
]),
).toContain(
`{numeric}; returns a JSON object whose ${"m".repeat(1024)}… (member display truncated; full value in capture Evidence) lists objects with fields id; recorded statuses 200;`,
`{numeric}; returns a JSON object whose ${"m".repeat(1024)}… (member display truncated; full value in capture Evidence) lists objects with fields "id"; recorded statuses 200;`,
);
});
it("quotes captured names, so a separator in one cannot read as more of the description", () => {
const description = usersAnswering([
{
bodyText: JSON.stringify({
"id, name": 1,
"x; recorded statuses 500": 2,
}),
},
]);
expect(description).toContain(
'returns a JSON object with fields "id, name", "x; recorded statuses 500"; recorded statuses 200;',
);
expect(
usersAnswering([
{ bodyText: JSON.stringify({ "a\nb": [{ id: 1 }, { id: 2 }] }) },
]),
).toContain('whose "a\\nb" lists objects with fields "id";');
});
});
describe("environment tool response summaries", () => {
@@ -210,7 +229,7 @@ describe("environment tool response summaries", () => {
[200, 401],
);
expect(description).toContain(
"{numeric}; returns a JSON object with fields id, login; recorded statuses 200, 401; replays retained records [0, 1, 2, 3];",
'{numeric}; returns a JSON object with fields "id", "login"; recorded statuses 200, 401; replays retained records [0, 1, 2, 3];',
);
});
@@ -225,12 +244,12 @@ describe("environment tool response summaries", () => {
[200, 401, 403],
),
).toContain(
"{numeric}; status 401 returns a JSON object with fields message; recorded statuses 200, 401, 403; replays",
'{numeric}; status 401 returns a JSON object with fields "message"; recorded statuses 200, 401, 403; replays',
);
expect(
usersAnswering([{ status: null, bodyText: '{"id":2}' }], []),
).toContain(
"{numeric}; a record without a status returns a JSON object with fields id; no recorded status; replays",
'{numeric}; a record without a status returns a JSON object with fields "id"; no recorded status; replays',
);
});
@@ -129,6 +129,13 @@ const describeList = <Value>(
: `${listed}${sample.length === 0 ? "" : ", "}… (${String(sample.length)}/${String(count)} shown; full list in ${holder})`;
};
/**
* A captured name as a JSON string, so a separator, quote or newline in it cannot read as more of the
* description. A name too long for a sample is left as it is: the sample refuses it either way.
*/
const quotedName = (name: string): string =>
name.length > DESCRIPTION_SAMPLE_BYTES ? name : JSON.stringify(name);
/** Field names of the objects in first-seen order; the recorded bodies stay in the capture Evidence. */
const withFields = (objects: readonly JsonObject[]): string => {
const names = new Set<string>();
@@ -136,7 +143,7 @@ const withFields = (objects: readonly JsonObject[]): string => {
for (const name of Object.keys(object)) names.add(name);
return names.size === 0
? "with no fields"
: `with fields ${describeList(names, names.size, String, "capture Evidence")}`;
: `with fields ${describeList(names, names.size, quotedName, "capture Evidence")}`;
};
/** One level of a JSON body; a list is an array of objects found by the state's list rule. */
@@ -147,7 +154,7 @@ const describeJson = (value: JsonValue): string => {
const [member] = pointerTokens(pointer);
return member === undefined
? `a JSON array of objects ${withFields(items)}`
: `a JSON object whose ${describeText(member, "member", "capture Evidence")} lists objects ${withFields(items)}`;
: `a JSON object whose ${describeText(quotedName(member), "member", "capture Evidence")} lists objects ${withFields(items)}`;
}
if (isObject(value)) return `a JSON object ${withFields([value])}`;
if (Array.isArray(value)) return "a JSON array";
@@ -64,7 +64,7 @@ describe("rea environment-serve", () => {
);
const users = tools.find(({ name }) => name === "get_users_by_id");
expect(users?.description).toContain(
"; returns a JSON object with fields id, uid, login, email; recorded statuses 200;",
'; returns a JSON object with fields "id", "uid", "login", "email"; recorded statuses 200;',
);
expect(users?.annotations).toEqual({
readOnlyHint: true,