Skip to content
Merged
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
28 changes: 28 additions & 0 deletions docs/research/WEB_STRUCTURED_DETAILS_2026-10-07.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Structured Web evidence integrity

- Status: validated source and production-function regression
- Created / verified: 2026-10-07
- Source: `origin/main` at `3cb2ecfe1bfbb98252651885311d309f83749428`
- Issue: [#695](https://github.com/openpi-dev/openpi/issues/695)
- Supersedes: none

## Verified facts

`projectMessage` dropped own `__proto__` keys in parsed JSON tool details by
assigning them through the ordinary object setter. This changed only the
projection object's prototype; global prototype pollution is not claimed.
Its shared bounded projection also silently omitted array elements or later
object properties when the 512-node work budget was exhausted exactly.
Four arrays of 128 numbers became arrays of lengths 128, 128, 128 and 123,
without a details-truncation marker. Regression tests failed on the source
baseline for both special-key loss and budget exhaustion.

## Fix and verification boundary

Own data properties are defined without invoking prototype setters. A remaining
item at an exhausted budget explicitly marks omission, while complete data at
the exact boundary remains retained. Existing policy omits partial details and
reports `truncation.details`; work, depth, byte and accessor limits remain.
The 25-test protocol suite passes after the fix. These are deterministic
production-function tests, not installed browser or model acceptance. Tool
execution, Session persistence and model context are unchanged.
58 changes: 58 additions & 0 deletions tests/web/protocol.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,64 @@ test("message projection keeps tool result correlation and details", () => {
});
});

test("structured tool details preserve special own JSON keys without changing prototypes", () => {
const details = JSON.parse(
'{"__proto__":{"evidence":"retained"},"constructor":"literal","prototype":{"__proto__":{"nested":true}}}',
);
const projected = projectMessage({
role: "toolResult",
content: "done",
details,
});
assert.equal(JSON.stringify(projected.details), JSON.stringify(details));
assert.ok(projected.details && typeof projected.details === "object");
assert.equal(Object.getPrototypeOf(projected.details), Object.prototype);
assert.equal(Object.hasOwn(projected.details, "__proto__"), true);
assert.equal(Object.hasOwn(Object.prototype, "evidence"), false);
assert.equal(Object.hasOwn(Object.prototype, "nested"), false);
assert.equal(projected.truncation?.details, undefined);
});

test("structured detail node exhaustion reports omitted array elements", () => {
const projected = projectMessage({
role: "toolResult",
content: "done",
details: Array.from({ length: 4 }, () => Array(128).fill(1)),
});
assert.equal(projected.details, undefined);
assert.equal(projected.truncation?.details, true);
});

test("structured detail node exhaustion reports omitted object properties", () => {
const projected = projectMessage({
role: "toolResult",
content: "done",
details: {
first: Array.from({ length: 3 }, () => Array(128).fill(1)),
last: Array(122).fill(1),
omitted: true,
},
});
assert.equal(projected.details, undefined);
assert.equal(projected.truncation?.details, true);
});

test("structured details exactly at the node boundary remain complete", () => {
const details = [
Array(128).fill(1),
Array(128).fill(1),
Array(128).fill(1),
Array(123).fill(1),
];
const projected = projectMessage({
role: "toolResult",
content: "done",
details,
});
assert.deepEqual(projected.details, details);
assert.equal(projected.truncation?.details, undefined);
});

test("native subagent display receipts survive projection while model follow-ups stay hidden", () => {
const details = {
results: [
Expand Down
19 changes: 16 additions & 3 deletions web/protocol/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -611,9 +611,12 @@ function boundedStructuredValue(
if (value.length > length) budget.truncated = true;
const projected: unknown[] = [];
for (let index = 0; index < length; index++) {
if (budget.nodes <= 0 || budget.bytes <= 0) {
budget.truncated = true;
break;
}
const item = boundedStructuredValue(value[index], budget, depth + 1);
if (item !== undefined) projected.push(item);
if (budget.nodes <= 0 || budget.bytes <= 0) break;
}
return projected;
}
Expand All @@ -625,15 +628,25 @@ function boundedStructuredValue(
break;
}
if (!Object.prototype.hasOwnProperty.call(value, key)) continue;
if (budget.nodes <= 0 || budget.bytes <= 0) {
budget.truncated = true;
break;
}
const descriptor = Object.getOwnPropertyDescriptor(value, key);
if (!descriptor || !("value" in descriptor)) {
budget.truncated = true;
continue;
}
const boundedKey = consumeStructuredText(key, budget);
const item = boundedStructuredValue(descriptor.value, budget, depth + 1);
if (item !== undefined) projected[boundedKey] = item;
if (budget.nodes <= 0 || budget.bytes <= 0) break;
if (item !== undefined) {
Object.defineProperty(projected, boundedKey, {
value: item,
enumerable: true,
configurable: true,
writable: true,
});
}
}
return projected;
}
Expand Down
Loading