I ended the last post on a high note. Semantic contracts let you describe behavior instead of signatures, and AI reasoning at runtime handles the routing. A notification capability doesn't care if the provider is ntfy or Slack or a custom webhook. The consumer says "notify the user with this message at this urgency" and the right thing happens.
That pattern works beautifully for a specific class of capability.
I've been calling these "delegated" capabilities. The consumer hands off a payload. The provider takes it, does its thing, returns confirmation. The consumer never touches the provider's internals. It never runs code inside the provider's environment. The contract is a handshake: here's what I need done, here's the shape of the request, go.
But some capabilities don't work like that.
I kept running into this with scheduling and browser automation. The consumer's logic needs to run inside the provider's execution context. The provider isn't just receiving a payload — it's hosting the consumer's behavior. And that changes everything about how composition works.
the stock checker problem
I have a plugin called Cardwatch. It monitors product stock at specific retailers — sneaker drops, limited-run collectibles, that kind of thing. The plugin's core value is its built-in knowledge of retailer drop schedules and patterns. It knows that retailer X drops every Wednesday at 9 PM, that retailer Y restocks in waves over 30-minute windows.
Cardwatch requires notification. That's non-negotiable — if it can't tell you something's in stock, it's useless.
It also optionally uses a browser. A browser lets Cardwatch open a retailer's page, parse the DOM, and check live stock status. That's a much richer experience than relying on schedule data alone. You get real-time confirmation instead of educated guesses.
On my local machine, everything works. I have a browser. I have local files. I have cron. Cardwatch checks retailer pages every few minutes, cross-references against its schedule data, and sends notifications through whatever notification provider I've installed.
Now I schedule that same plugin to run on a remote cron service.
The remote environment has no browser. No local filesystem. No GUI. The same plugin with the same capabilities declared can't do the same work. The scheduling contract is satisfied — something is triggering Cardwatch on a schedule. But the execution environment where Cardwatch runs is fundamentally different.
two kinds of composition
I think the distinction is clean once you see it. Notification is delegated: Cardwatch says "send this message," the provider handles delivery. Cardwatch doesn't care if the notification provider runs locally or on a server in Virginia. The contract is the contract.
Scheduling is environmental.
A scheduling provider executes Cardwatch's logic in the provider's own environment. Local cron runs Cardwatch on my machine — where browsers and files and everything else exist. A remote scheduling service runs Cardwatch on its machine. And its machine is deliberately limited.
The contract for scheduling might be identical in both cases: "run this task on this cron expression." But the execution environment is completely different. For delegated capabilities, you can swap providers and nothing breaks. For environmental capabilities, swapping providers can remove capabilities the consumer depends on.
the constraint is the point
My first instinct was to treat this as a problem. The remote environment is broken — it's missing things. I should figure out how to give it browser access, or at least a headless browser, or some kind of proxy.
I was wrong.
A remote scheduling provider with no filesystem access, no browser, and no local tools is more secure precisely because of those restrictions. The less an execution environment can do, the less attack surface it has. A sandboxed remote service that can send notifications but can't read my files isn't deficient. It's a security boundary.
I keep coming back to this. Restricted environments aren't broken environments. They're deliberately constrained. A cron service that can trigger tasks and send HTTP requests but can't open a browser or read a local disk is doing exactly what a cron service should do. Adding browser access to a remote execution environment would make it more capable and less secure at the same time.
The constraint is a feature.
graceful degradation through optional dependencies
This is where requires and optional start doing real work. Cardwatch requires notification — it literally can't function without a way to tell you about stock. Browser is optional. It adds live checking but the plugin works without it.
On my local machine: everything available, maximum functionality, maximum surface area. Cardwatch checks pages in real time, cross-references against schedule data, sends rich notifications with screenshots.
On the remote cron service: optional capabilities fall away. No browser means no live stock checking. But Cardwatch still knows that retailer X drops every Wednesday at 9 PM. It still sends notifications at the right times based on its built-in schedule data. The plugin is still useful. Just different.
Same plugin. Different environments. Different capability sets. Different security postures.
I find this forces a useful design discipline on plugin authors. You have to think about which capabilities are load-bearing and which are additive. If your plugin can't do anything without a browser, browser is a required dependency. If browser just adds richer functionality, it's optional. The dependency declaration becomes a statement about what the plugin fundamentally is.
Cardwatch is fundamentally a schedule-aware notification tool. Browser makes it better. But its identity doesn't depend on browser access.
composition gets harder
For delegated capabilities, the resolver's job is simple: does a provider exist that satisfies this contract? Find one, check that it's installed, done.
Environmental capabilities add a second question. Does this provider work here?
A scheduling provider on a restricted remote server satisfies the scheduling contract. But if the consumer also needs browser in that execution context, the composition fails — not because the contracts are incompatible, but because the environment can't support the full capability set.
The resolver needs to validate compatibility at configuration time. Not just "does this provider implement this contract?" but "can this consumer's logic actually run in this provider's execution environment?" That's a fundamentally different check. Contract compatibility is about interfaces. Environmental compatibility is about runtime constraints.
I think this is also where the "many simple systems composed" principle really matters. "Check stock and notify me on a schedule" sounds like one thing. It's actually three capabilities composed: scheduling, browser, notification. And the composition only works if the execution environment supports all three. If you'd built "scheduled browser-based stock checking with notifications" as a single monolithic capability, you'd have no way to degrade gracefully. It's all or nothing.
Keeping capabilities focused on one action means you can subtract them individually. Scheduling minus browser still leaves scheduling plus notification. That's a meaningful subset.
the spectrum
I've been thinking about this as a spectrum rather than a binary.
One end: fully local, everything available, maximum capability, maximum attack surface. The other end: fully sandboxed remote execution, minimal capability, minimal attack surface. Most real deployments land somewhere in the middle. A Docker container with network access but no GUI. A VM with a headless browser but no local file access. Each environment defines a set of available capabilities, and the plugin adapts.
The plugin author writes one plugin. The requires and optional declarations define the full range of functionality. The environment determines which slice of that range actually activates. No conditional logic in the plugin code — just capability declarations and graceful absence.
I don't have a great answer for how to communicate this to users. "Your plugin works but with reduced functionality" is accurate but not satisfying. I think the install plan needs to show what will work and what won't in a given environment, so the user can make an informed choice about where to run things. That's a UX problem I haven't solved yet.
what this means for the resolver
Three posts in and I've been building up an increasingly demanding set of requirements for whatever system resolves plugin dependencies.
Post 1: thin interfaces between plugins. Don't couple on implementation details. Post 2: semantic contracts instead of signatures. Describe behavior, let AI handle routing. And now: the resolver needs environment awareness. It needs to understand not just what a provider claims to do but where it can do it. It needs to match consumers to providers based on contract compatibility and environmental compatibility. It needs to generate install plans that account for graceful degradation — showing users which optional capabilities will be available in their specific environment.
That's a lot of work for a dependency resolver.
I've been building one. Next post, I'll show you what it looks like.