Skip to content

Extension tenancy ADR + capability contract (ADR 0006) - #140

Draft
geminixiang wants to merge 5 commits into
mainfrom
ext-tenancy-adr
Draft

geminixiang wants to merge 5 commits into
mainfrom
ext-tenancy-adr

Conversation

@geminixiang

Copy link
Copy Markdown
Owner

What

First shippable slice of the extension tenancy work — the analysis, the decision record, and the capability contract, leaving activation semantics untouched.

  • ADR 0006: names the tenancy model — three actors (deployment admin / office owner / extension author), three scopes (code always global, activation per-office, config+data follow activation), storage as namespaced handles, runtime authority follows the activation principal, and an honest statement that in-process extensions are fully trusted until a real isolation runtime exists. Deferred stages listed in order (activation records/rollout policy, api.state, activation-scoped secrets, service activations, out-of-process runtime).
  • Capability contract: package.json mikan.requires declares needed host capabilities; the loader checks against injected ExtensionHostServices before importing the module — a miss is one clear activation error, not a runtime throw at the first api.* call. api.capabilities.has/list replaces typeof api.schedules?.onCallback probing. Capability→service mapping has one home (CAPABILITY_PROBES in loader.ts). Names follow the api surface an author calls (messaging.notify, reactions), not host field names. mikan ext validate prints declared requires and warns on unknown names; activation hard-fails them.
  • Golden path: deploy/examples/extensions/scheduled-counter (one command + one callback schedule + per-conversation state, README explains the per-conversation activation model) and mikan ext init <name> scaffolds the same shape, immediately runnable via ext dev.

How it was built

Three independent implementations from one spec (this branch + two parallel agents in worktrees), then compared: this version won on gate compliance; the parallel versions' author-facing capability naming was adopted. Full comparison and research reviews under docs/research/design-review-2026-08/.

Not in this PR (deliberately)

  • Activation/rollout semantics unchanged — global install still activates everywhere (ADR stage 1).
  • No api.state primitive (stage 2), no secrets migration (stage 3), no service activations (stage 4).
  • agent-pm not migrated to declare requires — rides the next touch of that example.

Verification

Full gate green: lint, fmt:check, knip, build, 124 files / 1761 tests. ext initext validate exercised end-to-end in tests and manually.

…(ADR 0006)

Extensions declare host capabilities in package.json mikan.requires; the
loader checks declarations against the injected services before importing
the module, so a missing capability is one clear activation error instead
of a runtime throw at the first api call. api.capabilities.has/list lets
extensions degrade gracefully without typeof-probing. The capability→
service rule has one home (CAPABILITY_PROBES); validate reports declared
requires and warns on unknown names (activation hard-fails them).

deploy/examples/extensions/scheduled-counter is the golden-path example —
one command, one callback schedule, per-conversation state — and
mikan ext init scaffolds the same shape ready for ext dev.

ADR 0006 records the tenancy model (actors, scopes, deferred stages);
research reviews archived under docs/research/design-review-2026-08/.
…-off

Three independent implementations (A: this branch; B/C: parallel agents in
worktrees) were built from one spec and compared. A won on gate compliance
(B tripped knip, C shipped a type error in its example and a stale
public-api snapshot), but B/C converged on a better naming axis: capability
names should follow the api surface an author calls (api.notify →
messaging.notify, api.react → reactions), not host service field semantics.
Adopted. Comparison recorded in
docs/research/design-review-2026-08/extension-capability-impl-comparison.md.
…gent

Record the motivating principle behind the extension system: the agent's
tools are deliberately confined, and without a sanctioned outlet users
drill through the wall — the real case being a raw Slack API key placed
in a conversation vault so bash could curl the platform API, putting the
credential inside the model's reach. Extensions hold the means in host
code and expose only semantic surfaces; the model requests effects, never
credentials. This also grounds why extension code runs on the host, and
gives review its one-question heuristic.
Four independent agent perspectives — enterprise user advocate, extreme
skeptic, platform-ecosystem historian, agent-capability architect — argue
whether the extension system earns its keep. They converge unexpectedly:
in-process is deployment-trusted code and must be named as such; the
synchronous policy seam (pre-execution veto) is the one irreplaceable
core even the skeptic concedes; and the right ambition is an internal
application SDK for 5-20 audited extensions, not a marketplace. The
synthesis records the skeptic's routing test (when NOT to write an
extension), the user-demand gap map (platform events, identity queries,
portable interactions), and the architect's four control-plane gaps
(execution context, policy decisions, durable ops, credential handles) —
all inputs for the pending ADR 0006 revision.
geminixiang added a commit that referenced this pull request Aug 26, 2026
The extension route is frozen (PR #140 stays a draft): for 'let the agent
use service X', MCP is the cheaper boundary — servers are separate
processes speaking an external standard, so mikan carries no API-surface
maintenance and inherits the existing server ecosystem. The motivating
case stays the same as ADR 0006's: a user who wants Slack/API access
should configure a server holding the credential, not put a raw key
where the model can reach it.

- src/mcp: loadMcpTools connects settings-declared servers (stdio or
  streamable HTTP), wraps their tools as AgentTools named
  mcp__<server>__<tool>, and reports per-server failures without
  failing the rest; dispose closes clients with the runner.
- settings: mcpServers maps merge per server name across global and
  conversation scopes (conversation wins; disabled: true turns one off);
  a change refreshes cached runners like a model switch.
- admin portal: MCP panels on both tabs — add (stdio command or URL,
  env/header credentials), enable/disable, remove; the list endpoint
  redacts env/header values to key names.
- deps: @modelcontextprotocol/sdk ^1.30.0.

Tests spawn a real stdio server end-to-end (echo tool, env-passed
credential, isError-to-throw contract, unreachable-server isolation).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant