Skip to content

model-context: <shae-worker expose-to-model-context> warns about conflicting limits it never set #9

Closed

Description

@spearwolf

Summary

A <shae-worker expose-to-model-context> that joins an existing model-context registration warns about conflicting settings, even though the element never asks for any settings of its own. Chrome (with WebMCP, document.modelContext) logs once per exposing element:

ModelContext the tools under "shae-" are already registered with other limits or exposedTo; the values of the first call apply
{inEffect: {limits: {include: ["shadowObjects", "props"]}}, ignored: {limits: {}}}

The registration behaves correctly (the opener's limits apply), so the warning is pure noise. Because <shae-worker> has no way to pass limits, an application cannot get rid of it while combining both forms.

Version: @spearwolf/shadow-objects 0.35.2

Reproduction

import {exposeShadowEnvsToModelContext} from '@spearwolf/shadow-objects/model-context.js';

// 1. open the shared registration with capped answers, exposing nothing itself
await exposeShadowEnvsToModelContext({
  namespaces: [],
  limits: {include: ['shadowObjects', 'props']},
});

// 2. let an element join for its own namespace
document.querySelector('shae-worker').setAttribute('expose-to-model-context', '');
// → warning above, once per element

This is the documented combination: one call sets the limits and the elements join ("limits and exposedTo are the opener's", CHANGELOG 0.35.0).

Cause

exposeShadowEnvsToModelContext() normalises a missing limits to {} before joining:

// src/model-context/exposeShadowEnvsToModelContext.js
const settings = {limits: options.limits ?? {}, ...(options.exposedTo !== undefined ? {exposedTo: options.exposedTo} : {})};

joinSharedExposure() then compares the joiner's settings with the opener's by value:

// src/model-context/sharedExposure.js
const sameSettings = (a, b) =>
  JSON.stringify(a.limits) === JSON.stringify(b.limits) && JSON.stringify(a.exposedTo) === JSON.stringify(b.exposedTo);

So "no limits given" and "explicitly no limits ({})" look the same, and {} differs from the opener's limits. <shae-worker>'s #joinModelContextExposure() calls the function with signal, namespaces and redactProps only, so every element join counts as a conflict.

Proposed fix

Treat an omitted setting as "accept whatever the registration already has", and warn only about a setting the caller actually passed and that differs:

// exposeShadowEnvsToModelContext.js — keep "not given" distinguishable
const requested = {
  ...(options.limits !== undefined ? {limits: options.limits} : {}),
  ...(options.exposedTo !== undefined ? {exposedTo: options.exposedTo} : {}),
};

// sharedExposure.js — open() falls back to the defaults
const open = (modelContext, prefix, requested) => {
  const settings = {limits: requested.limits ?? {}, ...(requested.exposedTo !== undefined ? {exposedTo: requested.exposedTo} : {})};
  // … unchanged
};

// sharedExposure.js — compare only what the joiner asked for
const conflicts = (inEffect, requested) =>
  (requested.limits !== undefined && JSON.stringify(requested.limits) !== JSON.stringify(inEffect.limits)) ||
  (requested.exposedTo !== undefined && JSON.stringify(requested.exposedTo) !== JSON.stringify(inEffect.exposedTo));

and in joinSharedExposure() use else if (conflicts(exposure.settings, requested)), with ignored: requested in the log payload.

Effect:

  • <shae-worker expose-to-model-context> joins silently, whatever the opener set.
  • A call that explicitly passes different limits or exposedTo still gets the warning, which stays useful.
  • No API change. The opener's semantics are unchanged; an opener without limits still opens with {}.

Order dependence (related, optional)

When an element opens the registration first and an explicit exposeShadowEnvsToModelContext({limits}) call joins later, the explicit limits are dropped (with a warning). The element never chose those defaults, so it would be reasonable to let the first explicit limits win over defaults that were only implied. exposedTo is fixed at registerTool() time and cannot move, but the tools read ctx.limits on every call, so limits could be adopted late by turning ctx.limits into a getter on exposure.settings. Whether that is worth the extra rule is a design call; the fix above solves the reported noise on its own.

Tests worth adding

  • opener with limits, joiner without limits/exposedTo → no warning, opener's limits in effect
  • opener with limits, joiner with different explicit limits → warning as today
  • opener with exposedTo, element join → no warning

Workaround (consumer side)

Do not combine both forms: make one exposeShadowEnvsToModelContext({limits}) call without a namespaces rule (it then exposes every environment, including later ones) and leave the expose-to-model-context attribute off. This loses per-element redact-props.

Metadata

Metadata

Labels

bugSomething isn't working

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions