Connector dependencies — Requires, resolved fail-closed

Updated · View the entry on sijie.xyz ↗

The formal interface between the capabilities pillar and the connector pillar — fail-closed and consumer-agnostic by design. A plugin manifest declares Requires: ["calendar", "smtp"] — NAMES of in-app dependency providers, each backed by a connector category. capreg/depresolver.go is the registry; resolution happens at assembly: any required dep not Connected → the capability is globally hidden (fail-closed) — degraded discovery, not runtime error. Providers expose exactly two things: a Connected predicate and a call handle — never credentials (the connector-plugins boundary).

The consumer-agnostic decision makes the mechanism universal and bidirectional: the same named-dep resolution serves the agent, platform features, and future consumers (IM gateway, job-loop), in both directions (action and sync).

flowchart LR
  M["plugin manifest
Requires: [calendar]"] --> DR["capreg depresolver
(named providers)"]
  DR --> Q{"provider Connected?"}
  Q -- yes --> H["inject call handle
(never creds)"]
  Q -- no --> X["capability HIDDEN
(fail-closed)"]

Class view

classDiagram
  class DepProvider {
    <<interface>>
    +Name() string
    +Connected(ctx, ownerID) (bool, error)
  }
  class DepRegistry {
    -providers map[string]DepProvider
    +Register(p)
    +Lookup(name) (DepProvider, bool)
    +Unknown(names) []string
    +AllConnected(ctx, ownerID, names) (bool, error)
  }
  class RequiresDeps {
    <<interface>>
    +Requires() []string
  }
  class funcProvider {
    -name string
    -connected func(ctx, ownerID) (bool, error)
    built by NamedProvider(name, fn)
  }
  DepProvider <|.. funcProvider
  DepRegistry o-- DepProvider : by name
  RequiresDeps ..> DepRegistry : AllConnected at assembly

Precision note: DepProvider answers only Connected — it is a predicate, not the call handle. The actual handle a consumer uses is the category proxy (connector-plugins's CalendarProxy/MailProxy), wired separately; the dep mechanism just decides whether the capability is exposed at all (AllConnected false → hidden, fail-closed).

Since 2026-08-20 (a9c446937) there is a second predicate on top: OpProvider.CanPerform(ctx, ownerID, op) (depresolver.go:151), built by NamedOpProvider. A capability can require an operation (calendar:events.insert), not just a connection, so a read-only grant hides "book a meeting" but not "free slots". At 36789537d the composition root registers calendar as a NamedOpProvider (backed by ConnectorSlots.CanPerform) and smtp as a plain NamedProvider (cmd/server/axisconn/register.go:207-214) — those two names are the whole provider table.

Related notes