fix(plist): reserve Evidence framing in nesting allowance

This commit is contained in:
morluto committed 2026-11-09 14:58:35 +08:00
1 parent bd0642e0a4
commit 5d5fe72218
7 files changed
+27 -22

No files matched your search

+6 -5
View File
@@ -674,11 +674,12 @@ node of the decoder's DOM at a measured upper bound, so a plist made of many
small elements reaches it at a few MiB of source. JSON decode memory has not
been measured and has no equivalent decode estimate. A refused input reports non-retryable `resource_constraint` with
`reported_limits.maximum_input_bytes` or `maximum_decoded_bytes` as applicable.
Ordinary direct-source decoding has a ceiling of 998 source nesting levels,
derived from the existing 1000-level JSON result capacity by reserving one level
for typed scalar fields and one for the result's `value` field. Deeper input reports
`resource_constraint` with `reported_limits.maximum_nesting_depth: 998` and names
the fixed nesting allowance. XML admission also requires native validation, which
Ordinary direct-source decoding has a ceiling of 997 source nesting levels,
derived from the existing 1000-level JSON result capacity. Three levels are
reserved for typed scalar fields, the report value and its Evidence envelope.
Deeper input reports `resource_constraint` with
`reported_limits.maximum_nesting_depth: 997` and names the fixed nesting allowance.
XML admission also requires native validation, which
can refuse input below this decoding ceiling. Compatibility conversions can
likewise encounter an earlier native utility refusal; both retain native
diagnostics. Archive and Interface Builder decoding retain their separate
+2 -1
View File
@@ -1,3 +1,4 @@
import { MAX_NATIVE_PLIST_NESTING_DEPTH } from "../../domain/apple/plistValue.js";
import { requireOutputSchema } from "../toolOutputSchemaPrimitives.js";
import { z } from "zod";
import { nativeCallObservationInputSchema } from "../../domain/native/nativeCallObservation.js";
@@ -121,7 +122,7 @@ export const NATIVE_TOOL_CONTRACTS = [
),
native(
"inspect_plist",
"Requires an open binary or bundle-resource session; use open_binary or open_bundle_resources first. Parse its Info.plist by default, or select a local plist with path within that session. Supplying path does not open a session. Returns normalized JSON rather than plutil text. native_command_timeout_ms is each file or plutil command's wall clock (omitted means 60000 ms). The 67108864-byte plist input and decode allowances and the 998-level nesting allowance have no runtime override. XML sources also require native plutil validation, which can refuse shallower inputs with its own diagnostics. The 4096-byte markup probe is a probe window, not a document cap, and has no runtime override.",
`Requires an open binary or bundle-resource session; use open_binary or open_bundle_resources first. Parse its Info.plist by default, or select a local plist with path within that session. Supplying path does not open a session. Returns normalized JSON rather than plutil text. native_command_timeout_ms is each file or plutil command's wall clock (omitted means 60000 ms). The 67108864-byte plist input and decode allowances and the ${MAX_NATIVE_PLIST_NESTING_DEPTH}-level nesting allowance have no runtime override. XML sources also require native plutil validation, which can refuse shallower inputs with its own diagnostics. The 4096-byte markup probe is a probe window, not a document cap, and has no runtime override.`,
z.object({
path: localPathStringSchema
.optional()
+4 -1
View File
@@ -1,7 +1,7 @@
import { types } from "node:util";
import { z } from "zod";
import type { JsonValue } from "../jsonValue.js";
import { MAX_JSON_DEPTH, type JsonValue } from "../jsonValue.js";
import { copyPlistUidIdentity } from "./plistUid.js";
import { UnknownPlistScalar } from "./unknownPlistScalar.js";
@@ -18,6 +18,9 @@ const jsonScalarSchema = z.union([
*/
export const MAX_PLIST_DEPTH = 128;
/** Reserve typed scalar fields, the report value, and Evidence.normalized_result. */
export const MAX_NATIVE_PLIST_NESTING_DEPTH = MAX_JSON_DEPTH - 3;
/** Agent-visible refusal for nesting past the JSON projection allowance. */
const plistNestingLimitMessage = (maximumDepth = MAX_PLIST_DEPTH): string =>
`Plist exceeds ${String(maximumDepth)} nesting levels. This release has no runtime override for this ${String(maximumDepth)}-level nesting allowance.`;
+5 -3
View File
@@ -59,8 +59,8 @@ it.each([
] as const)(
"bounds ordinary %s by the projected JSON capacity, including scalar wrappers",
(encoding) => {
// One level for typed scalar fields and one for normalized_result.value.
const maximum = MAX_JSON_DEPTH - 2;
// Typed scalar fields, report.value and Evidence.normalized_result each add a level.
const maximum = MAX_JSON_DEPTH - 3;
for (const depth of [maximum, maximum + 1]) {
let binary: PlistValue = Buffer.from([0, 1]);
if (encoding === "binary data")
@@ -99,7 +99,9 @@ it.each([
} else {
if (!parsed.ok) throw parsed.error;
expect(() =>
jsonValueSchema.parse({ value: parsed.value.value }),
jsonValueSchema.parse({
normalized_result: { value: parsed.value.value },
}),
).not.toThrow();
let leaf: unknown = parsed.value.value;
for (let level = 0; level < depth; level += 1) {
+2 -4
View File
@@ -5,13 +5,14 @@ import {
AnalysisResourceConstraintError,
} from "../../domain/analysisErrorCore.js";
import { fixedAllowanceOptions } from "../../domain/fixedAllowanceRemediation.js";
import { MAX_JSON_DEPTH, type JsonValue } from "../../domain/jsonValue.js";
import type { JsonValue } from "../../domain/jsonValue.js";
import {
binaryDatePrecisionOverlapLimitation,
negativeZeroReal,
negativeZeroRealLimitation,
nonFiniteRealLimitation,
projectPlistValue,
MAX_NATIVE_PLIST_NESTING_DEPTH,
unknownDateLimitation,
unknownScalarLimitation,
type ProjectedPlistValue,
@@ -49,9 +50,6 @@ export const MAX_NATIVE_PLIST_BYTES = 64 * 1024 * 1024;
/** Provisional decode allowance. This release has no runtime override. */
const MAX_NATIVE_PLIST_DECODE_BYTES = 64 * 1024 * 1024;
// Reserve one level for typed scalar fields and one for normalized_result.value.
export const MAX_NATIVE_PLIST_NESTING_DEPTH = MAX_JSON_DEPTH - 2;
const mebibytes = (bytes: number): string =>
`${String(bytes / (1024 * 1024))} MiB`;
@@ -56,12 +56,12 @@ describe.skipIf(process.platform !== "darwin")(
{ format: "binary", depth: 200, data: false },
{ format: "xml", depth: 200, data: false },
{ format: "json", depth: 200, data: false },
{ format: "binary", depth: 997, data: true },
{ format: "binary", depth: 998, data: true },
{ format: "binary", depth: 999, data: true },
{ format: "xml", depth: 997, data: true },
{ format: "xml", depth: 998, data: true },
{ format: "xml", depth: 999, data: true },
{ format: "json", depth: 997, data: false },
{ format: "json", depth: 998, data: false },
{ format: "json", depth: 999, data: false },
] as const) {
const { format, depth, data } = fixture;
const path = join(root, `${format}-${depth}.plist`);
@@ -81,7 +81,7 @@ describe.skipIf(process.platform !== "darwin")(
// Compare its upper-edge admission with the actual utility, while
// requiring the historical depth-129/200 controls to succeed below.
const native =
format === "xml" && depth === 998
format === "xml" && depth === 997
? await lintXmlPlist(path)
: undefined;
const output = await cli.run({
@@ -90,7 +90,7 @@ describe.skipIf(process.platform !== "darwin")(
});
const response = await call("inspect_plist", { path });
expect(Buffer.compare(await readFile(path), bytes)).toBe(0);
if (depth === 999) {
if (depth === 998) {
expect(output.exitCode, output.stderr).toBe(1);
const expectedFailure = {
code: "resource_constraint",
@@ -98,7 +98,7 @@ describe.skipIf(process.platform !== "darwin")(
retryable: false,
details: {
resource: "memory",
reported_limits: { maximum_nesting_depth: 998 },
reported_limits: { maximum_nesting_depth: 997 },
},
remediation: {
action: expect.stringMatching(/\S/u),
@@ -96,8 +96,8 @@ it.each([
budget: "nesting",
value: nested(MAX_JSON_DEPTH - 1),
reason: /binary plist nesting.*decode budget/iu,
limits: { maximum_nesting_depth: MAX_JSON_DEPTH - 2 },
allowance: /998-level.*plist nesting.*no runtime override/iu,
limits: { maximum_nesting_depth: MAX_JSON_DEPTH - 3 },
allowance: /997-level.*plist nesting.*no runtime override/iu,
},
])(
"names the fixed binary plist $budget allowance instead of host memory",