Skip to content

Hardening: Enforce read-only guard for context-bundled integrations in the back-end #251

Description

@mattprintz

Summary

Integrations bundled with a context (skills, MCP servers) are meant to be read-only, but that guard is enforced only in the front-end. The back-end will execute mutation and deletion operations against context-bundled integrations whenever a request reaches it by any path other than the UI (a direct API call, a non-UI client, or a future front-end regression). This should be enforced server-side.

Background

Context-bundled integrations are namespaced under a context-<slug> corpus (SkillIntegrationProvider.from_context, MCPIntegrationProvider.from_context). The front-end keys "read-only" off exactly this: isContextProvidedIntegration() (beaker-vue/src/util/integration.ts) checks corpus.startsWith("context-"), and IntegrationsInterface.vue uses it to render a read-only viewer instead of an editor, so the UI never issues mutation requests.

The gap

The mutation path is: POST/DELETE /beaker/integrations/... (src/beaker_notebook/app/api/integrations.py) → call_in_context → the kernel action in src/beaker_notebook/lib/context.py (getattr(provider, function)(**kwargs)) → the provider's mutation method. No layer in this chain inspects the corpus prefix or any read-only flag. The providers are mutable, and their existing guards concern something else:

  • Skills guard on source_type (local/remote), not provenance. Context-bundled skills are generally local, so update_integration, remove_integration, add_resource, update_resource, and remove_resource all execute against them. remove_integration runs shutil.rmtree(skill.base_path) — deleting the skill directory shipped alongside the context.
  • MCP update_integration executes and calls server_config.update_config_file(), rewriting the context's own MCP config file. (Its resource/remove methods are NotImplementedError, so current exposure is narrower.)

A direct API call can therefore modify or delete context-bundled files on disk, bypassing the only guard that exists today.

Acceptance criteria

  • Mutation and deletion attempts against a context-bundled integration are rejected by the back-end regardless of the calling client, with a clear error surfaced to the caller.
  • The guard covers both skill and MCP providers, and both the integration- and resource-level mutation entry points.
  • A regression test exercises the back-end path directly (not just the UI) to confirm mutations on a context-<slug> integration are refused.

Design decisions (to settle during implementation)

  • Where the guard lives — centrally at the dispatch layer (call_in_context and/or the API handlers) vs. per-provider in the mutation methods. Provenance (corpus prefix) is currently the only signal distinguishing these integrations.
  • How read-only is represented — whether to model it explicitly (e.g. a flag on the integration) rather than re-deriving it from the corpus string in multiple places. Note that MutableBaseIntegrationProvider.mutable is per-provider-class, but read-only-ness here is per-integration (one skill provider serves both editable user skills and read-only context skills), so the class flag alone can't express it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    HardeningDefensive/robustness improvement to existing behavior

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions