fix(web): let a malformed _meta.ui stop the carrier lookup

uiMeta returned undefined for both "this bag declares no ui" and "its ui
is unusable", so a content item declaring `ui: null` fell through and was
granted whatever a BROADER carrier asked for — wrong precedence, wrong
direction to fail in. It now distinguishes the two: no `ui` key means
keep looking, a declared-but-malformed one decides for "no hints".

Also pin the full three-way precedence with a case carrying all three
carriers at once (no prior case would have caught the first two operands
being swapped), and state why a permission's object value need not be
empty: `{}` is the spec's extension point, so rejecting a populated one
would drop grants from servers written against a newer spec.

Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-23 22:05:23 -04:00
commit 53943c2ef8
2 changed files with 117 additions and 29 deletions

View file

@ -107,6 +107,15 @@ describe("approveSandboxPermissions", () => {
warn.mockRestore();
});
it("accepts a non-empty object, since `{}` is the spec's extension point", () => {
// The key's presence is the request; a field the host doesn't know can't
// narrow it, and rejecting it would drop grants from servers written
// against a newer spec. The value is normalized to `{}` on the way out.
expect(approveSandboxPermissions({ camera: { enabled: false } })).toEqual({
camera: {},
});
});
it("drops `false` without warning, and ignores unknown keys", () => {
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
expect(

View file

@ -106,13 +106,21 @@ const PERMISSION_KEYS = exhaustivePermissionKeys([
* the `unknown` parameter: this is the boundary that decides what the shape is,
* not a consumer of an already-validated one.
*
* A key counts as requested only when it carries the spec's empty-object marker
* (`camera: {}` — {@link McpUiResourcePermissions} defines every permission that
* way). Everything else fails closed: `true`, `false`, any string, `null`, a
* number, an array, and any key outside the four the proxy knows. `true` in
* particular looks like a harmless shorthand, but honoring an undocumented one
* means a server typo turns an iframe permission ON, which is the wrong
* direction to guess in.
* A key counts as requested only when its value is an **object** — the marker
* {@link McpUiResourcePermissions} defines for every permission is `{}`, and
* presence of that object is the request. Everything else fails closed:
* `true`, `false`, any string, `null`, a number, an array, and any key outside
* the four the proxy knows. `true` in particular looks like a harmless
* shorthand, but honoring an undocumented one means a server typo turns an
* iframe permission ON, which is the wrong direction to guess in.
*
* The object is not required to be *empty*. `{}` is a type with no fields yet,
* not a promise there will never be any — it is precisely the spec's extension
* point, so rejecting `{ camera: { … } }` would fail closed the day a
* permission gains an option, silently dropping grants from servers written
* against the newer spec. The value is normalized to `{}` on the way out
* either way, so an unrecognized field can't reach the proxy; what it cannot
* do is *narrow* a grant the key's presence already asked for.
*
* Returns undefined when nothing survives, so the notification and the
* `hostCapabilities.sandbox` echo carry no permissions at all rather than an