Add an opt-in --gateway mode to the ai plugin that centralizes AI provider access and MCP-OAuth custody behind one NetScript service, sibling to (never replacing) the default in-process path.
Context
Part of epic #238 · cluster plugin-ai · wave defer · depends-on P1, P2, E1
This is the last node in the plugin-ai DAG: it only becomes buildable once the in-process default emitters exist (P1, P2) and the @netscript/ai core contract (E1) is stable enough to implement against. It is deferred because most single-instance deployments never need centralized custody — this slice exists for the scaled/multi-instance case.
Problem / motivation
Without this slice, every emitter call goes straight to the provider in-process, and any MCP-OAuth token or provider secret has to live wherever the calling instance runs. That is fine for one instance; it is exactly the secret-sprawl / hand-rolled-proxy pattern eis-chat fell into once more than one process needed the same authenticated provider session — no single place owns token lifecycle, refresh, or revocation. The doctrine's answer for this class of problem is a thin, opt-in centralizing service (same shape as workers-api in the workers plugin), not a bespoke sidecar. SO-7 specifically calls for centralized MCP-OAuth custody; today nothing in @netscript/ai or the ai plugin provides it.
Scope
Ships
plugins/ai/services/src/main.ts — createPluginService shell over @netscript/ai, implementing aiContract as an HTTP service entrypoint (mirrors plugins/workers/services/src/main.ts shape).
plugins/ai/src/public/mod.ts — conditional .withService({ name: 'ai-gateway', entrypoint: './services/src/main.ts', port: <assigned> }), registered only when the plugin is configured with --gateway (or the equivalent manifest flag); absent entirely in default mode.
- Centralized MCP-OAuth custody module behind the gateway service: single token/secret store and refresh path, consumed only by the gateway process (SO-7).
- Emitter proxy mode: the in-process emitters from P1/P2 detect
--gateway and forward calls to the gateway service over the same aiContract surface instead of calling providers directly.
Out of scope
- Default in-process AI path (emitters calling providers directly) — owned by P1/P2.
@netscript/ai core contract, aiContract shape, and provider subpaths (@netscript/ai/anthropic, etc.) — owned by E1 and sibling ENGINE-cluster provider slices.
- OTel/
TelemetryPort wiring — owned by the ./otel slice (E9).
- fresh-ui chat primitives and
ui:add ai registration — owned by FB0.
- Named orchestration strategies — explicitly out of v1 per the ratified primitives-only decision.
Public surface
plugins/ai/services/src/main.ts (new): gateway service entrypoint exporting the createPluginService-built server implementing aiContract.
plugins/ai/src/public/mod.ts: conditional .withService(...) contribution, only present when --gateway is set; manifest otherwise unchanged from P1/P2.
- MCP-OAuth custody port/module used exclusively by the gateway service (path finalized against the P1/P2 plugin-ai layout); no new symbols added to
@netscript/ai itself — this slice only consumes aiContract, it does not extend it.
Acceptance & fitness gates
Dependencies
depends-on: P1, P2, E1 (GitHub numbers tracked in the #238 DAG checklist).
Size
L
Part of #238
Add an opt-in
--gatewaymode to theaiplugin that centralizes AI provider access and MCP-OAuth custody behind one NetScript service, sibling to (never replacing) the default in-process path.Context
Part of epic #238 · cluster plugin-ai · wave defer · depends-on P1, P2, E1
This is the last node in the plugin-ai DAG: it only becomes buildable once the in-process default emitters exist (P1, P2) and the
@netscript/aicore contract (E1) is stable enough to implement against. It is deferred because most single-instance deployments never need centralized custody — this slice exists for the scaled/multi-instance case.Problem / motivation
Without this slice, every emitter call goes straight to the provider in-process, and any MCP-OAuth token or provider secret has to live wherever the calling instance runs. That is fine for one instance; it is exactly the secret-sprawl / hand-rolled-proxy pattern eis-chat fell into once more than one process needed the same authenticated provider session — no single place owns token lifecycle, refresh, or revocation. The doctrine's answer for this class of problem is a thin, opt-in centralizing service (same shape as
workers-apiin the workers plugin), not a bespoke sidecar. SO-7 specifically calls for centralized MCP-OAuth custody; today nothing in@netscript/aior theaiplugin provides it.Scope
Ships
plugins/ai/services/src/main.ts—createPluginServiceshell over@netscript/ai, implementingaiContractas an HTTP service entrypoint (mirrorsplugins/workers/services/src/main.tsshape).plugins/ai/src/public/mod.ts— conditional.withService({ name: 'ai-gateway', entrypoint: './services/src/main.ts', port: <assigned> }), registered only when the plugin is configured with--gateway(or the equivalent manifest flag); absent entirely in default mode.--gatewayand forward calls to the gateway service over the sameaiContractsurface instead of calling providers directly.Out of scope
@netscript/aicore contract,aiContractshape, and provider subpaths (@netscript/ai/anthropic, etc.) — owned by E1 and sibling ENGINE-cluster provider slices.TelemetryPortwiring — owned by the./otelslice (E9).ui:add airegistration — owned by FB0.Public surface
plugins/ai/services/src/main.ts(new): gateway service entrypoint exporting thecreatePluginService-built server implementingaiContract.plugins/ai/src/public/mod.ts: conditional.withService(...)contribution, only present when--gatewayis set; manifest otherwise unchanged from P1/P2.@netscript/aiitself — this slice only consumesaiContract, it does not extend it.Acceptance & fitness gates
deno task arch:checkpasses forplugins/ai.plugins/ai/src/public/mod.ts) carries JSDoc +@module, <=20 exports.plugins/aipublishes viadeno task publish:dry-runWITHOUT--allow-slow-types.stop(),AbortSignal-driven shutdown), consistent with the workers service reference.scaffold.runtimecase: scaffold a project with theaiplugin configured--gateway, boot Aspire, confirm theai-gatewayservice starts, an in-process emitter call proxies through it successfully, and the default (non-gateway) scaffold path is unaffected.--gatewaymode carry no OAuth material locally (SO-7).--gatewayis unset.Dependencies
depends-on: P1, P2, E1 (GitHub numbers tracked in the #238 DAG checklist).
Size
L
Part of #238