You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
0.0.7 normative scope amendment — 2026-08-13. RFC 0001 Stage 2 supersedes conflicting rows
below. This issue ships the public contribution descriptors/tuple algebra and defaulted client/query
context generics, plus the stable-v1 implementation under the private src/internal/client-contributions/ adapter. It must cover key algebra, reconnect preparation,
desktop rejection, cache modes, conflicts, and local failure taxonomy. It must not publicly
export createHttpClientLink, ClientLinkPort, ClientLinkCallOptions, or any internal
adapter port; must not accept upstream interceptor/plugin callback arrays; and must keep port
and timeout accepted/deprecated rather than remove them. #451 remains the sole future custom-link
owner and #1351 owns transport consolidation/no-op option migration.
Summary
CreateServiceClientOptions is a closed nine-field record and ServiceClientContext is a closed
interface, so no consumer can add a header, an interceptor, a link plugin, a custom fetch, or a
typed per-call context field. The link factory that would let them work around it is
package-private, even though the ports module's own doc comment advertises "the transport seam". On
the server side RPCHandlerConfig.plugins exists but the builder never populates it, and the plugin
type it accepts hands unknown to plugin authors. The result is that the only supported way to send
one extra header is to fork ~90 lines of framework internals per app — and silently lose NetScript's
client span in the process.
Source at baseline fac9e339042c (re-verified for this draft):
packages/sdk/src/ports/service-client.ts:203-222 — nine fields: contract, serviceName, routerName, protocol, apiPath, apiVersion, port, timeout, propagateTraceContext. No headers / fetch / interceptors / plugins / link / context
parameter.
packages/sdk/src/ports/service-client.ts:129-155 — ServiceClientContext is a concrete
interface (signal, cache, retry knobs, traceHeaders); :160-171 hard-codes it into ServiceClientMethod.
packages/sdk/src/client/service-client.ts:41-49 — port and timeout are never destructured; packages/sdk/src/presets/define-services.ts:106-116 forwards both into that ignoring function,
so the dead options look live at L2 and L3.
packages/sdk/src/client/mod.ts:15-36 — exports only createServiceClient, isDefinedError, safe, and types. createHttpClientLink is private.
packages/sdk/src/ports/mod.ts:7 — "…discovery metadata, and the transport seam" — while packages/sdk/src/ports/client-link-factory.ts:18-25 (ClientLinkPort, ClientLinkCallOptions) is not exported from that module.
packages/service/src/types.ts:216-225 — ServiceHandlerPlugin.init?(options: unknown, router: unknown); packages/logger/orpc-plugin.ts:11-42 redeclares shim option types because of it.
packages/plugin/src/config/domain/plugin-contributions.ts:12-39 — no client-side contribution
group.
Upstream (pinned 1.14.6, surface identical to 1.14.15 per research/external/orpc.md §0): StandardLinkOptions (interceptors, clientInterceptors, plugins), LinkFetchClientOptions (fetch, adapterInterceptors, plugins), StandardRPCLinkCodecOptions (headers, method, fallbackMethod, maxUrlLength, customJsonSerializers), ClientContext as a free type parameter.
Current surface
One constructor with no seams. createServiceClient builds rpcPath, calls the private createHttpClientLink, and wraps the result in createORPCClient
(packages/sdk/src/client/service-client.ts:41-66). Consumers get exactly what that function
decided: two frozen client plugins, one header set, one fetch. @netscript/sdk/desktop proves the
link is swappable in principle (packages/sdk/src/desktop/application/desktop-rpc-client.ts:18-20
builds a MessagePort RPCLink with customJsonSerializers) and equally closed in practice. On the
server, withRPC() (packages/service/src/builder/service-builder.ts:91-104) and DefineServiceOptions (packages/service/src/presets/define-service.ts:112-143) accept no plugins, so a plugin that wants a server oRPC plugin must drop to L1 and hand-mount into Hono.
Target contract
Per RFC-A §3.1–§3.5, §3.9, §3.10:
Client construction opens.CreateServiceClientOptions<TContract, TWith> gains an optional,
ordered with?: TWith contribution chain and a mutually exclusive link?: ClientLinkPort<…>
escape hatch. Omitting both yields byte-identical behaviour and types to today.
Context becomes a parameter.ServiceClient<TContract, TContext> and ServiceClientMethod<TInput, TOutput, TError, TContext>; BaseServiceClientContext is today's
shape; ServiceClientContext survives one minor as a @deprecated alias.
The transport seam is exported.createHttpClientLink from @netscript/sdk/client; ClientLinkPort / ClientLinkCallOptions from @netscript/sdk/ports — closing the
doc-vs-export contradiction at packages/sdk/src/ports/mod.ts:7.
Framework defaults become contributions. Retry, dedupe, and the CLIENT span are composed
through the same public path, so there is no private fast lane.
Dead options die.port and timeout are removed from CreateServiceClientOptions and DefineServiceConfig; timeout is re-expressed as a contribution over AbortSignal.timeout.
Server reachability + typing.withRPC() and DefineServiceOptions accept plugins and warnOnlyCodes and pass them to createRPCHandler; ServiceHandlerPlugin is typed to upstream's init?(options: StandardHandlerOptions<T>, router: Router<any, T>); the never-read deduplication option is either wired or removed.
Env boundary. Contribution environment is checked at construction; server-only in a
browser build throws.
Acceptance
createServiceClient accepts an ordered tuple of version-1 contribution descriptors whose
public powers are limited to typed context projection, disjoint header keys/values, and declared
response-cache behavior.
Omitting the tuple preserves today's wire behavior and existing call sites compile unchanged.
Client/query context generics default compatibly and compose to the intersection of declared
contribution contexts.
The stable-v1 adapter lives under packages/sdk/src/internal/client-contributions/; neither createHttpClientLink nor ClientLinkPort/ClientLinkCallOptions is publicly exported.
Contributions cannot supply or observe fetch, link plugins, interceptor arrays, retry, dedupe,
tracing, or the resolved HTTP method; those remain transport-owned.
Construction rejects duplicate names/header ownership, unsupported contract versions, more than
16 contributions, invalid dependency ordering, and desktop-incompatible contributions with
deterministic errors naming the conflicting descriptors.
Reconnect preparation and cache modes are covered by positive and red-first negative tests;
removing a descriptor removes its context/header/cache effect.
Server handler/plugin forwarding, handler option typing, and RPC deduplication are out of this
client-seam leaf unless independently required by the RFC's private adapter.
deno task check, deno task test, and deno task publish:dry-run pass with isolated
declarations intact.
Do not touch the query-key algebra, createQueryFactory's dropped abort signal, or nested
routers — T2 generation pack.
Do not touch PluginContractRouter = object or the Hono-vs-oRPC middleware seam — T3.
Docs/consumer proof
docs/site/services-sdk/sdk.md and docs/site/reference/sdk/index.md gain one worked contribution
example that compiles under the docs gate; packages/sdk/README.md's export table lists the newly
exported transport seam; and the "escape hatch = fork the link" paragraph is deleted rather than
softened. Consumer proof is that an app can add one header without importing anything from @netscript/telemetry and without losing its client span — assert the span is still emitted in the
same test that asserts the header.
Provenance
Seed run plan-fable5-remediation-roadmap--seed, PR #1347, 2026-08-08. Sourced from research/repo-audit/services-sdk.md (S4, S11, S12, S13, S21), research/external/orpc.md (G2, G3,
G6, G7) and research/repo-audit/auth.md (G1); all cited lines re-verified against worktree baseline fac9e339042c. No GitHub mutation was performed.
Filed from planning seed PR #1347 · source Draft-ID T1-02 · live issue #1349.
Important
0.0.7 normative scope amendment — 2026-08-13. RFC 0001 Stage 2 supersedes conflicting rows
below. This issue ships the public contribution descriptors/tuple algebra and defaulted client/query
context generics, plus the stable-v1 implementation under the private
src/internal/client-contributions/adapter. It must cover key algebra, reconnect preparation,desktop rejection, cache modes, conflicts, and local failure taxonomy. It must not publicly
export
createHttpClientLink,ClientLinkPort,ClientLinkCallOptions, or any internaladapter port; must not accept upstream interceptor/plugin callback arrays; and must keep
portand
timeoutaccepted/deprecated rather than remove them. #451 remains the sole future custom-linkowner and #1351 owns transport consolidation/no-op option migration.
Summary
CreateServiceClientOptionsis a closed nine-field record andServiceClientContextis a closedinterface, so no consumer can add a header, an interceptor, a link plugin, a custom
fetch, or atyped per-call context field. The link factory that would let them work around it is
package-private, even though the ports module's own doc comment advertises "the transport seam". On
the server side
RPCHandlerConfig.pluginsexists but the builder never populates it, and the plugintype it accepts hands
unknownto plugin authors. The result is that the only supported way to sendone extra header is to fork ~90 lines of framework internals per app — and silently lose NetScript's
client span in the process.
Evidence
research/repo-audit/services-sdk.md§2.1–§2.4, §3.1–§3.3, §3.6, gap registerS4/S11/S12/S13/S21;
research/external/orpc.md§4 (G2, G3, G6, G7), §5;research/repo-audit/auth.md§0 proof 1–2, §2.fac9e339042c(re-verified for this draft):packages/sdk/src/ports/service-client.ts:203-222— nine fields:contract, serviceName, routerName, protocol, apiPath, apiVersion, port, timeout, propagateTraceContext. Noheaders/fetch/interceptors/plugins/link/ contextparameter.
packages/sdk/src/ports/service-client.ts:129-155—ServiceClientContextis a concreteinterface (
signal,cache, retry knobs,traceHeaders);:160-171hard-codes it intoServiceClientMethod.packages/sdk/src/client/service-client.ts:41-49—portandtimeoutare never destructured;packages/sdk/src/presets/define-services.ts:106-116forwards both into that ignoring function,so the dead options look live at L2 and L3.
packages/sdk/src/client/http-client-link.ts:82(method:literal),:82-101(headersliteral:
Content-Type+ optional trace),:102-126(pluginsliteral:ClientRetryPlugin{retry:0}+DedupeRequestsPluginwith frozen filter/groups),:127+(hard-coded
fetchcarrying the CLIENT span).packages/sdk/src/client/mod.ts:15-36— exports onlycreateServiceClient,isDefinedError,safe, and types.createHttpClientLinkis private.packages/sdk/src/ports/mod.ts:7— "…discovery metadata, and the transport seam" — whilepackages/sdk/src/ports/client-link-factory.ts:18-25(ClientLinkPort,ClientLinkCallOptions) is not exported from that module.packages/service/src/primitives/handlers.ts:41-58—RPCHandlerConfigdeclaresplugins,tracing,errorHandling,deduplication,warnOnlyCodes,logging,debug;packages/service/src/builder/service-rpc.ts:57callscreateRPCHandler(options?.rpcRouter ?? router, { serviceName, debug })— nothing else is everpassed.
packages/service/src/types.ts:216-225—ServiceHandlerPlugin.init?(options: unknown, router: unknown);packages/logger/orpc-plugin.ts:11-42redeclares shim option types because of it.packages/plugin/src/config/domain/plugin-contributions.ts:12-39— no client-side contributiongroup.
research/external/orpc.md§0):StandardLinkOptions(interceptors,clientInterceptors,plugins),LinkFetchClientOptions(fetch,adapterInterceptors,plugins),StandardRPCLinkCodecOptions(headers,method,fallbackMethod,maxUrlLength,customJsonSerializers),ClientContextas a free type parameter.Current surface
One constructor with no seams.
createServiceClientbuildsrpcPath, calls the privatecreateHttpClientLink, and wraps the result increateORPCClient(
packages/sdk/src/client/service-client.ts:41-66). Consumers get exactly what that functiondecided: two frozen client plugins, one header set, one
fetch.@netscript/sdk/desktopproves thelink is swappable in principle (
packages/sdk/src/desktop/application/desktop-rpc-client.ts:18-20builds a MessagePort
RPCLinkwithcustomJsonSerializers) and equally closed in practice. On theserver,
withRPC()(packages/service/src/builder/service-builder.ts:91-104) andDefineServiceOptions(packages/service/src/presets/define-service.ts:112-143) accept noplugins, so a plugin that wants a server oRPC plugin must drop to L1 and hand-mount into Hono.Target contract
Per RFC-A §3.1–§3.5, §3.9, §3.10:
CreateServiceClientOptions<TContract, TWith>gains an optional,ordered
with?: TWithcontribution chain and a mutually exclusivelink?: ClientLinkPort<…>escape hatch. Omitting both yields byte-identical behaviour and types to today.
ServiceClient<TContract, TContext>andServiceClientMethod<TInput, TOutput, TError, TContext>;BaseServiceClientContextis today'sshape;
ServiceClientContextsurvives one minor as a@deprecatedalias.createHttpClientLinkfrom@netscript/sdk/client;ClientLinkPort/ClientLinkCallOptionsfrom@netscript/sdk/ports— closing thedoc-vs-export contradiction at
packages/sdk/src/ports/mod.ts:7.through the same public path, so there is no private fast lane.
portandtimeoutare removed fromCreateServiceClientOptionsandDefineServiceConfig;timeoutis re-expressed as a contribution overAbortSignal.timeout.withRPC()andDefineServiceOptionsacceptpluginsandwarnOnlyCodesand pass them tocreateRPCHandler;ServiceHandlerPluginis typed to upstream'sinit?(options: StandardHandlerOptions<T>, router: Router<any, T>); the never-readdeduplicationoption is either wired or removed.environmentis checked at construction;server-only in abrowser build throws.
Acceptance
createServiceClientaccepts an ordered tuple of version-1 contribution descriptors whosepublic powers are limited to typed context projection, disjoint header keys/values, and declared
response-cache behavior.
contribution contexts.
packages/sdk/src/internal/client-contributions/; neithercreateHttpClientLinknorClientLinkPort/ClientLinkCallOptionsis publicly exported.tracing, or the resolved HTTP method; those remain transport-owned.
portandtimeoutremain accepted and deprecated. Their migration/no-op disposition belongsto [sdk-client S4] refactor(sdk): centralize HTTP method and GET-cache policy #1351.
16 contributions, invalid dependency ordering, and desktop-incompatible contributions with
deterministic errors naming the conflicting descriptors.
removing a descriptor removes its context/header/cache effect.
client-seam leaf unless independently required by the RFC's private adapter.
deno task check,deno task test, anddeno task publish:dry-runpass with isolateddeclarations intact.
Boundaries
safe/isDefinedErrororbaseContracthere — that is [sdk-client S1] fix(sdk): preserve contract errors through safe() and isDefinedError #1350.seam feat(sdk): in-process link-mode adapter for single-process service mounting #451 needs; feat(sdk): in-process link-mode adapter for single-process service mounting #451 stays open and separate.
PluginContributionsclient group is added here rather than in [sdk-client S5] feat(sdk/auth): prove typed credential contributions end to end #1352, it must not use a closedliteral union like
plugin-contributions.ts:16.gateway).
createQueryFactory's dropped abort signal, or nestedrouters — T2 generation pack.
PluginContractRouter = objector the Hono-vs-oRPC middleware seam — T3.Docs/consumer proof
docs/site/services-sdk/sdk.mdanddocs/site/reference/sdk/index.mdgain one worked contributionexample that compiles under the docs gate;
packages/sdk/README.md's export table lists the newlyexported transport seam; and the "escape hatch = fork the link" paragraph is deleted rather than
softened. Consumer proof is that an app can add one header without importing anything from
@netscript/telemetryand without losing its client span — assert the span is still emitted in thesame test that asserts the header.
Provenance
Seed run
plan-fable5-remediation-roadmap--seed, PR #1347, 2026-08-08. Sourced fromresearch/repo-audit/services-sdk.md(S4, S11, S12, S13, S21),research/external/orpc.md(G2, G3,G6, G7) and
research/repo-audit/auth.md(G1); all cited lines re-verified against worktree baselinefac9e339042c. No GitHub mutation was performed.Filed from planning seed PR #1347 · source Draft-ID T1-02 · live issue #1349.