@setu-ts/diagnostics-plugin
The authenticated local diagnostics connector (M98b): connects a native devtool client to the kernel diagnostics (M98a) over a separate, runtime-owned authenticated IPv4 loopback HTTP listener. Deno and Node, with bounded polling only. No browser UI, no application-data reads, no application-control commands, and authentication is independent of any devtool subscription.
Trust model — read this first
- Loopback is not authentication. The HMAC-SHA-256 signed protocol is. Every request and response byte is MAC-verified with a fresh per-launch session key.
- Loopback is not encryption. A privileged local process can sniff authenticated bytes. Remote tunnels, shared untrusted hosts, and production use are outside this transport’s threat model.
- No environment fallback. Importing the package, registering unrelated plugins, or setting an environment variable never exposes an endpoint. Activation requires explicit plugin options.
- Trusted application code and installed in-process plugins already run with application privileges; this is not a sandbox against a compromised process.
Installation
import { DiagnosticsPlugin } from '@setu-ts/diagnostics-plugin';
No third-party dependency. The listener socket is owned by @setu-ts/runtime (RuntimePlugin is a
required dependency of every application and now provides
CAPABILITIES.LOCAL_DIAGNOSTICS_LISTENER).
Per-session rate buckets are swept on authenticated promotion using a doubling threshold starting at 64 sessions. A bucket is discarded only after it has refilled to its full burst; a drained or partially refilled bucket keeps its budget.
Pairing
The trusted native launcher generates a fresh 32-byte session key and a 16-byte session ID per launch and passes them to the application’s composition in memory (its environment handoff is the launcher’s trust boundary; application subprocesses must not inherit these values). Sharing one pair across applications is unsupported. The plugin copies the key into a non-extractable Web Crypto HMAC key and zeroes its temporary copy.
Usage
import { createApplication } from '@setu-ts/kernel';
import { RuntimePlugin } from '@setu-ts/runtime';
import { DiagnosticsPlugin } from '@setu-ts/diagnostics-plugin';
// Fresh per-launch credentials, generated here for the example; in a real
// launch the trusted native launcher supplies them in memory.
const sessionId = Array.from(crypto.getRandomValues(new Uint8Array(16)), (b) =>
b.toString(16).padStart(2, '0'),
).join('');
const sessionKey = crypto.getRandomValues(new Uint8Array(32));
const diagnostics = DiagnosticsPlugin({
enabled: true, // explicit opt-in; false/omitted refuses activation
port: 4919, // 1024–65535, IPv4 loopback only, no auto-selection
sessionId, // 16 random bytes as 32 lowercase hex, per launch
sessionKey, // 32 random bytes, per launch
ttlMs: 900_000, // optional; 15 minutes default, max 1 hour
maxSessionLifetimeMs: 28_800_000, // optional renewal cap; max 12 hours awake time
});
const app = createApplication({
plugins: [RuntimePlugin(), diagnostics],
diagnostics: {}, // M98a kernel diagnostics must be enabled
});
await app.start();
// Later, without stopping the application:
await diagnostics.revoke();
Activation refusals are startup failures: missing kernel diagnostics, an unsupported runtime, an invalid port, or invalid credentials. Port conflicts fail closed — no scan, no fallback address.
Development-only composition
enabled is an acknowledgement, not a toggle. There is no disabled mode: enabled: false never
activated anything, it is refused at composition time. So the obvious line for “run it outside
production only” —
DiagnosticsPlugin({ enabled: !isProduction, … })
— would not give you an inert connector in production. It would give you an application that throws
at composition and never boots. enabled is typed as the literal true so that shape is a compile
error rather than a production outage. Decide by inclusion.
The decision has to be made at createApplication() time — the kernel’s collector is built in the
constructor and diagnostics is a construction option — so the cleanest shape is a development-only
entry point that production never imports:
import type { IApplication, IPlugin } from '@setu-ts/common';
import { createApplication, type KernelDiagnosticsOptions } from '@setu-ts/kernel';
import { RuntimePlugin } from '@setu-ts/runtime';
import { DiagnosticsPlugin } from '@setu-ts/diagnostics-plugin';
// setu.config.ts — no devtool import anywhere in the production graph.
//
// The composition is the SECOND parameter deliberately — and it is spelled
// `devtool`, because this is the exact shape `setu new --devtool` and
// `setu devtool enable` emit since M98c. `setu commands` builds the
// application to discover plugin-contributed verbs, and it calls this factory
// with its own inert discovery env as the FIRST positional argument on every
// target (`app-loader.ts`) — so a single-parameter `createApp(extra?)` would
// receive that proxy as `extra` and throw on the spread below.
export function createApp(
_env?: Readonly<Record<string, unknown>>,
devtool?: { plugins?: readonly IPlugin[]; diagnostics?: KernelDiagnosticsOptions },
): IApplication {
return createApplication({
plugins: [RuntimePlugin(), ...(devtool?.plugins ?? [])],
...(devtool?.diagnostics !== undefined ? { diagnostics: devtool.diagnostics } : {}),
});
}
// main.ts — production. Never imports @setu-ts/diagnostics-plugin.
export const production: IApplication = createApp();
// main.dev.ts — the ONLY module that imports the connector.
const sessionId = Array.from(crypto.getRandomValues(new Uint8Array(16)), (b) =>
b.toString(16).padStart(2, '0'),
).join('');
const sessionKey = crypto.getRandomValues(new Uint8Array(32));
export const development: IApplication = createApp(undefined, {
plugins: [DiagnosticsPlugin({ enabled: true, port: 4919, sessionId, sessionKey })],
diagnostics: {},
});
That makes exclusion a property of the build rather than of a runtime branch: the production entry cannot enable the connector, because it never imports it.
Credentials come from the trusted launcher through the child process environment (see
Pairing); the plugin reads no environment variable itself, so nothing auto-enables.
setu new --devtool and setu devtool enable emit exactly this composition — the development
entry, its dev task, and a check task that reaches main.dev.ts.
Optional hardening: a scoped network grant
A generated project’s start task carries a BARE --allow-net. Scoping it to the application port
is OPTIONAL hardening for a project with no egress — not a default, and not a guarantee you already
have. On Deno 2.9.6 an allowlist governs OUTBOUND connections as well as bind (measured): under
--allow-net=0.0.0.0:3000, a fetch to any other address fails with NotCapable the same way a
bind does, so a scoped grant refuses every database, broker and outbound API call the project makes.
If you scope it anyway, you get a second, independent guarantee on top of the build-level isolation
above: a bind on 127.0.0.1:4919 fails under the start grant with
NotCapable: Requires net access to "127.0.0.1:4919", so even a connector that reached production
by mistake cannot open its port. The loopback restriction itself is the listener’s, not a permission
flag’s — the runtime-owned listener binds 127.0.0.1 before anything else and refuses a
non-loopback bind outright.
Protocol
Six signed GET operations — /v1/status, /v1/snapshot, /v1/events?after=<N>&limit=<N>, and the
inspector operations /v1/health (M98d), /v1/config (M98e) and /v1/queues?after=<N>&limit=<N>
(M98f) — over bounded polling. The status body carries an inspectors manifest (health,
configuration and queues implemented; the rest reserved and false until their own operations
ship); a client paired against a legacy three-field status body resolves it to all-false, and its
health(), configuration(), queues() and scheduler() answer a typed unsupported without
sending the request. Requests authenticate with X-Setu-Session, X-Setu-Sequence (strictly
monotonic), X-Setu-Instance, and X-Setu-Mac (HMAC-SHA-256 over canonical newline-joined fields,
verified via subtle.verify). Responses are signed over their exact bytes; the client verifies
BEFORE parsing anything. Bodies are bounded (256 KiB), events are capped at 128 per read, and a
fixed set of value-free error codes (invalid-request, unauthorized, expired,
unsupported-version, unavailable, rate-limited) never reflects input. See
docs/diagnostics-protocol.md in the repository for the complete wire specification and fixtures.
Bounds: at most 8 simultaneous handlers with one slot reserved against unpaired floods, an anonymous refusal budget of 5 requests/second (burst 10), a per-session budget of 20 requests/second (burst 40), and an 8 KiB header budget. Expiry uses the runtime’s monotonic clock.
Native client
createDiagnosticsClient is the reviewed client-side implementation, consumed by the separately
maintained devtool. Every dependency is injected (subtle, fetch, a timing port); calls are
serialized with strictly increasing sequence numbers; the initial pairing exchange is terminal on
failure; response MACs verify over the exact bounded bytes before parsing.
client.session() returns the authenticated remaining lifetime. When its renewal member is not
null, client.renew() extends the same session — same key, session ID, instance binding and
sequence space — by at most one ttlMs window and never beyond the activation-time
maxSessionLifetimeMs. Schedule renewal with margin, normally after half of expiresInMs. Omitting
maxSessionLifetimeMs preserves the legacy status bytes and expiry timer and makes renew() throw
locally without probing /v1/renew.
The M98d inspector operation is read through client.health(): Promise<HealthDiagnosticsSnapshot> —
the minimized health-observation snapshot the health plugin registers under
CAPABILITIES.HEALTH_DIAGNOSTICS. A connector without a health source answers a typed
unsupported; the client surfaces the snapshot’s state rather than failing the call.
The M98e inspector operation is read through
client.configuration(): Promise<ConfigDiagnosticsSnapshot> — the value-free provenance snapshot
the config plugin registers under CAPABILITIES.CONFIG_DIAGNOSTICS (always registered, so “no
config plugin” answers unsupported where “present but off” answers disabled). Every string from
the config plugin is an application-approved display alias, and the connector and client both refuse
an alias carrying a control character, whoever registered the source: no configuration value, hash,
length, raw key name, or file path is ever carried, and unapproved keys are never observed at all.
The M98f queue inspector is read through
client.queues(after, limit?): Promise<QueueDiagnosticsBatch>. The cursor is the CONNECTOR’s merge
cursor: every QueuePlugin instance contributes its own queue-diagnostics source, and each queue read
drains every source’s new attempts into one bounded merge ring before serving the page. The batch
carries one status per source (a q<N> id, the approved instance alias, a per-source lost for
attempts that source’s own ring evicted before the connector drained them), the page of attempts,
and every source’s latest depths. With no queue plugin registered the connector answers
state: 'unsupported'.
The M98l realtime inspector is read through
client.realtime(): Promise<RealtimeDiagnosticsResponse>. Each WebSocket, SSE and
realtime-backplane plugin contributes one source with a fixed sourceKind; websocket and sse
sources carry current openConnections/groups gauges read from the plugin’s own service, a
backplane reports its gauges as unsupported, and a source that cannot be read appears as a
value-free snapshot of kind unknown. With no realtime plugin registered the connector answers
state: 'unsupported'.
The M98n outbound HTTP inspector is read through
client.outboundHttp(): Promise<OutboundHttpDiagnosticsResponse>. Each createObservedFetch helper
from @setu-ts/sdk whose plugin the application registered contributes one source counting the
attempts made through that helper’s fetch — never a URL, header, body or error. With no helper
registered the connector answers state: 'unsupported'. The M98m storage inspector is read through
client.storage(): Promise<StorageDiagnosticsResponse>, the same aggregate shape as the M98i cache
inspector. Each StoragePlugin instance contributes one source; a source that cannot be read appears
as a value-free collection-failed snapshot. With no storage plugin registered the connector
answers state: 'unsupported'.
The full public surface is documented in PUBLIC_API.md.
Privacy
The connector serves M98a’s minimized projections only: allowlisted labels, opaque ids, bounded counts, value-free failure codes. No request bodies, headers, credentials, payloads, logs, or environment values ever cross the protocol. Diagnostic counters are in-process and value-free.
The M98d health inspector carries the same discipline: each observation holds only the approved
display alias, the framework’s own status, the outcome state, and monotonic timing. An indicator’s
data, the thrown value of a failure, and any absolute time are never projected — the canary test
plants both and asserts their absence at the source, in the raw signed bytes captured below the
client, and in the client DTO. The connector validates the projected DTO before signing it; a source
that violates it answers collection-failed.
The M98e configuration inspector holds it too: canary values planted in an unapproved key, inside
the approved file, and inside an expanded reference’s resolution are asserted absent at all three
layers, as are the raw key names and the configured path. droppedEntries counts only budget
omissions — a count of unapproved keys would disclose that they exist, so there is none.
The M98i cache inspector (GET /v1/cache, client.cache()) serves every CachePlugin instance’s
operation counters — per fixed operation, under each instance’s approved alias — from at most 16
multi-provider sources resolved at bootstrap (more refuses startup). Keys, prefixes, values, URLs,
factory results and errors never reach any layer; the e2e canary plants each and asserts its absence
in the source snapshot, the raw signed bytes and the client DTO.
The M98k scheduler inspector (GET /v1/scheduler, client.scheduler()) serves every
SchedulerPlugin instance’s execution observations — contended, lock-failed and dispatched fires with
wall-clock lateness, and handler attempts with their retry count — from at most 16 multi-provider
sources resolved at bootstrap. Job names, cron expressions, payloads, job ids, lock keys and thrown
errors never reach any layer; the e2e canary plants each and asserts its absence in the source
snapshot, the raw signed bytes and the client DTO, and proves a lock loser produces no handler
record. A skipped local fire is never reported as a globally missed execution. The M98m storage
inspector (GET /v1/storage, client.storage()) serves every StoragePlugin instance’s operation
counters — per fixed operation, under each instance’s approved alias — from at most 16
multi-provider sources resolved at bootstrap (more refuses startup), the M98i cache rule. Object
paths, stored bytes, content types, signed URLs and error text never reach any layer; the e2e canary
plants each and asserts its absence in the source snapshot, the raw signed bytes and the client DTO.
The M98g trace inspector (GET /v1/traces?after=N&limit=N, client.traces(after, limit?)) serves
the TelemetryPlugin’s completed, sampled spans under the same rules: only approved operation
aliases, W3C-validated identifiers, at most eight link identifier pairs, kind, outcome, duration and
monotonic age. Span names, attributes, events, resource labels, tracestate, baggage, exceptions and
status messages never reach any layer — the e2e canary plants a hostile span name and a hostile
attribute and asserts their absence in the batch, the raw signed bytes and the client DTO.
Correlation joins EQUAL trace ids across independently authenticated sessions; an identifier grants
no discovery or connection authority, parentVisibility never fabricates an edge, and ageMs is
arrival age at one process, never a global timeline.
Exports
| Export | Kind |
|---|---|
createDiagnosticsClient | function |
DiagnosticsPlugin | function |
DiagnosticsClientOptions | interface |
DiagnosticsPluginOptions | interface |
DiagnosticsSessionLifetime | interface |
IDiagnosticsClient | interface |
IDiagnosticsPlugin | interface |
Generated from the package barrel by pnpm run docs:exports; pnpm run check:docs fails when it
drifts.
API reference
Diagnostics Connector (@setu-ts/diagnostics-plugin)
Authenticated promotion periodically reclaims per-session rate buckets that have refilled to their full burst. Sweeps use doubling thresholds starting at 64; drained and partially refilled buckets keep their limits.
An authenticated local connector between a native devtool client and M98a’s kernel diagnostics,
served over a runtime-owned IPv4 loopback listener. Deno and Node, with bounded polling only; no
browser UI, no application data, no control commands, and no environment fallback. Activation is
explicit:
DiagnosticsPlugin({ enabled: true, port, sessionId, sessionKey, ttlMs?, maxSessionLifetimeMs? }) —
an omitted or false enabled, an invalid port or credential, an unsupported runtime, or an
application without M98a diagnostics refuses activation. enabled is typed as the LITERAL true,
not boolean: there is no disabled mode, so enabled: !isProduction — which would throw at
composition and stop a production application booting — is a compile error rather than an outage.
Keep the connector out of production by deciding whether to CONSTRUCT it, ideally from a
development-only entry point the production graph never imports; the package README has the worked
composition and the Deno permission grant that refuses the bind independently. Loopback is not
authentication (the HMAC-SHA-256 signed protocol is) and not encryption (a privileged local sniffer
reads authenticated bytes; remote and production use are unsupported).
| Export | Kind | Since |
|---|---|---|
DiagnosticsPlugin | function | 0.8.0 |
IDiagnosticsPlugin | interface | 0.8.0 |
DiagnosticsPluginOptions | interface | 0.8.0 |
createDiagnosticsClient | function | 0.8.0 |
IDiagnosticsClient | interface | 0.8.0 |
DiagnosticsClientOptions | interface | 0.8.0 |
DiagnosticsSessionLifetime | interface | 0.8.0 |
IDiagnosticsPlugin.revoke() immediately disables authorization, drops key references, and closes
the listener without stopping the parent application; it is idempotent, and a revoked instance never
reactivates. createDiagnosticsClient({ endpoint, sessionId, sessionKey, subtle, fetch, timing })
requires endpoint to be exactly http://127.0.0.1:<port>; the client performs the signed status
pairing automatically, serializes calls with strictly increasing sequence numbers, verifies every
response MAC over the exact bounded bytes before parsing, and marks failed pairing terminal. After
pairing, every response’s signed x-setu-instance header and every body instanceId (core snapshot
and event batch included) must equal the paired instance; a mismatch is the fixed connection error.
Every body is checked against its exact DTO before it is returned — the core snapshot’s node and
edge records and the event batch’s events and cursor included (see
docs/diagnostics-protocol.md) — and every result is deeply frozen.
A middleware node’s priority and an event’s statusCode are left unranged because the application
sets both: either may be any finite number (a status is not necessarily a valid HTTP status), and
the kernel omits a non-finite value rather than letting it serialize to null.
Session renewal (M98o). maxSessionLifetimeMs explicitly opts a session into renewal; it must
be an integer from ttlMs through 43,200,000 (12 hours of awake, monotonic time). Without it,
status bytes and timer behavior are unchanged. With it, signed status adds
renewal: { maxRemainingMs }, client.session(): Promise<DiagnosticsSessionLifetime> returns the
fresh authenticated lifetime, and client.renew() extends the same key/session/instance/sequence
space by at most one TTL window without crossing the activation-time cap. Both returned lifetime
values are deeply frozen non-negative safe integers and expiresInMs <= maxRemainingMs; renewal
support is represented by renewal === null, not an exported error type. Consumers should renew at
roughly half the remaining window. The connector serves the authenticated, replay-protected
GET /v1/renew; the client still treats every non-200 as its fixed connection error and never
parses unsigned refusal bodies.
Health observations (M98d). The status body now carries an inspectors manifest —
{ health: true, configuration: true, queues: true, traces: true, authorization: true, cache: true, scheduler: true, events: true, realtime: true, storage: true, outboundHttp: true }
(configuration is true since M98e, queues since M98f, traces since M98g, cache since M98i,
authorization since M98h, events since M98j, scheduler since M98k, realtime since M98l,
storage since M98m and outboundHttp since M98n, all below) — and the connector serves a first
inspector operation, GET /v1/health. The client reads it through
client.health(): Promise<HealthDiagnosticsSnapshot>. The connector resolves the optional health
source under CAPABILITIES.HEALTH_DIAGNOSTICS once, at registration: an absent source answers a
typed unsupported snapshot (no indicator runs, startup never fails), a registered-but-disabled
source answers disabled, and a throwing source answers a value-free collection-failed snapshot —
as does a source whose projected DTO fails the exact validator (an unknown enum, a non-finite or
negative measurement, an oversized alias, more than 64 observations, a malformed shape), so nothing
unvalidated is ever signed — none of which changes the application’s readiness. The snapshot is the
health plugin’s minimized DTO (approved alias, framework status, outcome state, monotonic timing
only — no indicator data, no error text, no absolute time), projected field-by-field and bounded
by the same 256 KiB response ceiling as every other operation. A client paired against a legacy M98b
status body (no manifest) resolves all inspectors to false and its health() answers
unsupported without sending the request. The full wire shape is in docs/diagnostics-protocol.md.
Queue observations (M98f). The connector serves GET /v1/queues?after=<N>&limit=<N> (the same
canonical query grammar as /v1/events), read through
client.queues(after: number, limit?: number): Promise<QueueDiagnosticsBatch> — same argument
bounds as read() (non-negative safe-integer cursor, limit 1–128, default 128). At bootstrap the
connector reads every IQueueDiagnosticsSource registered under CAPABILITIES.QUEUE_DIAGNOSTICS
(every QueuePlugin instance contributes one as a multi provider; at most 16 are read, the rest
counted in truncatedSources). Because one session is permitted, the CONNECTOR keeps one cursor per
source: each authenticated queue read drains every source’s new attempts, in registration order,
into a bounded 1,024-event merge ring, then serves the requested page of that ring under M98a’s
cursor contract (exclusive after, exact lost for merge-ring eviction, an empty page echoes its
cursor, a cursor beyond the merge sequence is refused invalid-request). A source ring that wrapped
between two reads is reported on THAT source’s status lost, never in the batch lost. A source
that throws or answers a batch failing the connector’s exact validator is reported
collection-failed with failure: 'source-read-failed' and nothing it returned is merged; the
merged frame runs the exact QueueDiagnosticsBatch validator before it is signed, and depths —
never events — are trimmed to keep it within 256 KiB (truncatedDepths). With no queue source
registered the batch is state: 'unsupported'; a client whose negotiated manifest has
queues: false answers that frozen batch, echoing its cursor, without sending the request.
IDiagnosticsClient.queues is a new REQUIRED member — additive for callers; a structural
implementation of IDiagnosticsClient must add it (the package has not yet been published).
Configuration provenance (M98e). The connector serves the config plugin’s value-free provenance
snapshot at GET /v1/config, read through
client.configuration(): Promise<ConfigDiagnosticsSnapshot>. The source is resolved under
CAPABILITIES.CONFIG_DIAGNOSTICS — the ConfigPlugin registers one EAGERLY, always, so “no config
plugin” (typed unsupported) is distinguishable from “present but off” (the plugin’s own disabled
answer). A throwing source, or one whose projected DTO fails the exact validator (an unknown origin
or schema effect, a sourceAlias on a non-file origin, an oversized alias, an alias carrying a
C0/C1 control character, an over-budget alias array, a malformed shape), answers a value-free
collection-failed snapshot, so nothing unvalidated is ever signed; the native client runs the same
validator. The connector also re-checks the instance binding on the projected copy it signs, so a
source whose instanceId reads differently the second time is refused rather than signed, and it
copies every source-supplied list itself — each item read once, never through the source’s own map
or toJSON — so the validator checks exactly what is signed. The same rules apply to
GET /v1/health. Every string from the first-party source is an application-approved display alias:
no configuration value, value hash, value length, raw key name, or file path is ever carried, and
unapproved keys are never observed at all — no counter discloses that they exist. The negotiated
manifest governs configuration() exactly as it governs health(): a legacy pairing answers
unsupported without sending the request.
Scheduler observations (M98k). The connector serves GET /v1/scheduler, read through
client.scheduler(): Promise<SchedulerDiagnosticsResponse>. At bootstrap it resolves every
ISchedulerDiagnosticsSource registered under CAPABILITIES.SCHEDULER_DIAGNOSTICS (each
SchedulerPlugin instance contributes one as a multi provider) and refuses to start with more
than 16. Each authenticated read calls every source’s synchronous snapshot() once, copies only
plain own data properties, and isolates a throwing or malformed source as a value-free
collection-failed snapshot; duplicate non-null SOURCE aliases or a body over 256 KiB collapse the
response to collection-failed with no sources. With no source the response is unsupported; a
client whose negotiated manifest has scheduler: false answers that locally without a request.
IDiagnosticsClient.scheduler is a new REQUIRED member — additive for callers; a structural
implementation must add it. The wire shape is in docs/diagnostics-protocol.md.
Cache observations (M98i). The connector serves GET /v1/cache, read through
client.cache(): Promise<CacheDiagnosticsResponse>. At bootstrap it resolves every
ICacheDiagnosticsSource registered under CAPABILITIES.CACHE_DIAGNOSTICS (each CachePlugin
instance contributes one as a multi provider) and refuses to start with more than 16. Each
authenticated read calls every source’s synchronous snapshot() once, copies only plain own data
properties, and isolates a throwing or malformed source as a value-free collection-failed
snapshot; duplicate non-null aliases or a body over 256 KiB collapse the response to
collection-failed with no sources. With no source the response is unsupported; a client whose
negotiated manifest has cache: false answers that locally without a request.
IDiagnosticsClient.cache is a new REQUIRED member — additive for callers; a structural
implementation must add it. The wire shape is in docs/diagnostics-protocol.md.
Storage observations (M98m). The connector serves GET /v1/storage, read through
client.storage(): Promise<StorageDiagnosticsResponse> — the same aggregate shape, the same
s1…s16 session-local ids and the same more-than-16-refuses-startup rule as the M98i cache
operation. At bootstrap it resolves every IStorageDiagnosticsSource registered under
CAPABILITIES.STORAGE_DIAGNOSTICS (each StoragePlugin instance contributes one as a multi provider)
and refuses to start with more than 16. Each authenticated read calls every source’s synchronous
snapshot() once, copies only plain own data properties, and isolates a throwing or malformed
source as a value-free collection-failed snapshot; duplicate non-null aliases or a body over 256
KiB collapse the response to collection-failed with no sources. With no source the response is
unsupported; a client whose negotiated manifest has storage: false answers that locally without
a request. IDiagnosticsClient.storage is a new REQUIRED member — additive for callers; a
structural implementation must add it. The wire shape is in docs/diagnostics-protocol.md.
Trace observations (M98g). The connector serves GET /v1/traces?after=<N>&limit=<N> (the same
canonical query grammar as /v1/events), read through
client.traces(after: number, limit?: number): Promise<TraceDiagnosticsBatch> — same argument
bounds as read(). The TelemetryPlugin ALWAYS registers one ITraceDiagnosticsSource under
CAPABILITIES.TRACE_DIAGNOSTICS: without its diagnostics option it answers disabled; with the
option but a custom tracerProviderFactory or no exporter it answers unsupported with the fixed
coverage reason (custom-provider / noop-no-provider); with the option on the built-in OTel
provider an additional span processor — appended AFTER the exporter processor in the same provider
constructor, never wrapping it, never exporting, never throwing into OTel, and closed only by the
provider’s own shutdown — minimizes every finished SAMPLED span to the approved field set before
retention. Only exact raw span names listed in TraceDiagnosticsOptions.operations are observed,
each replaced by its approved alias (the M98d alias shape: 1–64 UTF-8 bytes, no control characters,
unique; at most 128 operations, serviceAlias never derived from the OTel resource). Records carry
sequence, the two aliases, W3C-validated trace/span/parent identifiers (all-zero rejected;
parentSpanId present exactly when parentVisibility is observed or remote-or-unobserved), at
most eight validated link identifier pairs, kind, outcome (ok/error/unset), durationMs,
ageMs and parentVisibility — never the span name, attributes, events, resource labels,
tracestate, baggage, exceptions or a status message. The batch reports state, coverage,
instrumentation (the Node-only families whose registry outcome said enabled), sampler (the
configured description or unknown), M98a’s cursor contract over a 1,024-record ring (lost is per
batch and exact), and a saturating droppedSpans counter. A parent is an identifier relationship
only — no edge is fabricated — and ageMs describes arrival at ONE process, never a global
timeline; correlation joins equal trace ids across independently authenticated sessions and grants
no discovery authority. A source that throws or fails the exact validator is answered
collection-failed; with no source the batch is unsupported with coverage: 'unknown'; a client
whose negotiated manifest has traces: false answers that frozen batch, echoing its cursor, without
sending the request. IDiagnosticsClient.traces is a new REQUIRED member — additive for callers; a
structural implementation of IDiagnosticsClient must add it (the package has not yet been
published).
Authorization decision explanations (M98h). The connector serves
GET /v1/authorization?after=<N>&limit=<N> (the same canonical query grammar as /v1/events), read
through
client.authorization(after: number, limit?: number): Promise<AuthorizationDiagnosticsBatch> — same
argument bounds as read(). AuthPlugin ALWAYS registers one IAuthorizationDiagnosticsSource
under CAPABILITIES.AUTHORIZATION_DIAGNOSTICS: without its authorizationDiagnostics option it
answers disabled; with the option it observes the first-party RbacService — the boolean
IAuthorizationService surface and every guard’s status, body and short-circuit behaviour are
unchanged — and answers no-data/ready while it does. The observer records, per decision, only
the approved role and permission aliases (raw names, principal identifiers, request paths and error
text are never retained), the per-step outcome, the fixed reason vocabulary, the evaluation count
actually performed — a compound that short-circuits reports the steps it really took, never a
fabricated full sweep — stepsTruncated (a compound over 16 steps is RETAINED with its real result
and the true stepsEvaluated, its step list holding only the first 16), and ageMs. ruleAliases
is the set of DISTINCT requested rules in first-requested order (at most 128); stepsEvaluated
counts every evaluated occurrence, so a request repeating a rule may report more evaluated steps
than it names rules. A decision whose requested rule lacks an approved alias is dropped before
buffering and counted in a saturating droppedUnapproved; partial rule lists are never emitted. An
unapproved granting role never drops a decision — it is omitted, leaving no viaRoleAlias. The
source re-verifies its provider at read time through the new optional
IServiceRegistry.isCurrent?(token, instance) identity predicate — non-resolving, never returning a
service, never instantiating a lazy factory — and LATCHES unsupported terminal:
coverage: 'provider-identity-unavailable' when the registry lacks the predicate,
coverage: 'custom-provider' when the current CAPABILITIES.AUTHORIZATION provider is not the
exact RbacService instance the plugin created. A direct custom-service decision is never guessed
from booleans or status; latching clears retained state and never resumes. A source that throws or
fails the exact validator answers collection-failed; a client whose negotiated manifest has
authorization: false answers the frozen unsupported batch, echoing its cursor, without sending
the request. IDiagnosticsClient.authorization is a new REQUIRED member — additive for callers; a
structural implementation of IDiagnosticsClient must add it (the package has not yet been
published). IServiceRegistry.isCurrent is OPTIONAL so third-party registry-shaped implementations
keep compiling; its absence conservatively disables explanations rather than falling back to a
resolving get.
Event dispatch observations (M98j). The connector serves GET /v1/event — a SNAPSHOT operation
with no query, unlike the paged kernel-event stream /v1/events (one letter apart; the manifest key
events names THIS inspector) — read through client.events(): Promise<EventDiagnosticsResponse>.
The EventsPlugin ALWAYS registers one IEventDiagnosticsSource under
CAPABILITIES.EVENTS_DIAGNOSTICS as a multi provider (it is an optional dependency of the
connector, not a claimed capability, and never appears in the plugin’s provides): without the
plugin’s diagnostics option it answers disabled; with the option the collector behind the bus
reduces each dispatch to counters BEFORE anything is retained. Only event types whose exact raw name
appears in EventsDiagnosticsOptions.events are observed, each replaced by its approved alias (1–64
UTF-8 bytes, no controls, unique, at most 64 entries, own-property lookup); records aggregate per
(alias, operation) — publish (every publication, including no-subscriber ones) and handler (each
existing handler’s await; async publication completion is not handler completion) — over at most 64
slots with 60-second retention, a 30-second stale threshold and saturating counters; started is
counted when a boundary begins and count when it settles, so started - count is work in flight.
Observation never changes dispatch: a thrown errorHandler propagates exactly as without
diagnostics, and a failing clock latches the source collection-failed without rejecting a publish.
Event payloads, event ids, aggregate ids, handler names, unapproved type names and error text never
enter a record. At bootstrap the connector reads every registered source in registration order (more
than 16 refuses startup with a fixed configuration error), assigns session-local positional
sourceId values s1…s16, collapses a body over the 256 KiB budget to the fixed
collection-failed response with no sources, answers a throwing or validator-refusing source with its
own value-free collection-failed snapshot, and answers duplicate non-null aliases with a fixed
collection-failed response listing NO sources; the aggregate state is ready if any source is
ready, otherwise collection-failed, stale, no-data, disabled. A client whose negotiated
manifest has events: false answers a frozen unsupported response without sending the request.
IDiagnosticsClient.events is a new REQUIRED member — additive for callers; a structural
implementation of IDiagnosticsClient must add it (the package has not yet been published).
Realtime lifecycle observations (M98l). The connector serves GET /v1/realtime — a SNAPSHOT
operation with no query — read through client.realtime(): Promise<RealtimeDiagnosticsResponse>.
The WebSocket, SSE and realtime-backplane plugins each ALWAYS register one
IRealtimeDiagnosticsSource under CAPABILITIES.REALTIME_DIAGNOSTICS as a multi provider (never in
provides): without their diagnostics option it answers disabled, and a 'custom' backplane is
never observed. Each snapshot carries a fixed sourceKind. A websocket or sse source counts
open, close and send at the connection and reads current gauges (openConnections and
groups — rooms or channels) from the plugin’s OWN service on every authenticated read; it is
ready whenever those were read, even after every record expired. A backplane source counts
backplane-publish (the transport’s publish() resolving — never peer delivery — with its
duration) and backplane-receive (after the transport’s own shape and origin filters), has
unsupported gauges, and is no-data, ready or stale by record age. backpressureCloses is a
number only on an sse close record (SSE backlog-guard closes, 0 when none) and null
everywhere else; lastDurationMs is a number only on backplane-publish. Records expire 60 seconds
after their last observation. No frame, message, close reason, connection id, room or channel name,
header, query, principal, Last-Event-ID or backplane origin enters a record. The connector reads
the sources at bootstrap (more than 16 refuses startup), assigns positional s1…s16, answers a
throwing or validator-refusing source with a value-free snapshot of kind unknown, collapses
duplicate non-null aliases or an over-budget body to a fixed collection-failed response with no
sources, and refuses any kind/operation, kind/gauge or backpressure combination outside these rules
— and so does the client. A client whose negotiated manifest has realtime: false answers a frozen
unsupported response without sending the request. IDiagnosticsClient.realtime is a new REQUIRED
member — additive for callers; a structural implementation must add it.
Outbound HTTP attempt observations (M98n). The connector serves GET /v1/outbound-http — a
SNAPSHOT operation with no query — read through
client.outboundHttp(): Promise<OutboundHttpDiagnosticsResponse>. Sources come from the SDK’s
createObservedFetch helper: its plugin registers one IOutboundHttpDiagnosticsSource under
CAPABILITIES.OUTBOUND_HTTP_DIAGNOSTICS as a multi provider (never in provides). A source holds
at most one record counting started, count, responses and failures, with lastStatusClass
('2xx' | '3xx' | '4xx' | '5xx' | 'other', null until a response), lastDurationMs (time to
headers, null until a settlement) and ageMs. The connector reads the sources at bootstrap (more
than 16 refuses startup), assigns s1…s16, answers a throwing or invalid source with a value-free
collection-failed snapshot, collapses duplicate non-null aliases or an over-budget body to a fixed
no-source response, and refuses any snapshot breaking responses + failures === count,
count <= started or the state rules — and so does the client. A client whose negotiated manifest
has outboundHttp: false answers a frozen unsupported response without sending the request.
IDiagnosticsClient.outboundHttp is a new REQUIRED member.
The listener side ships in @setu-ts/common + @setu-ts/runtime: RuntimePlugin provides
ILocalDiagnosticsListenerFactory under CAPABILITIES.LOCAL_DIAGNOSTICS_LISTENER
(LocalDiagnosticsListenerOptions { port, handler } → ILocalDiagnosticsListener.close()), binding
exactly one 127.0.0.1 listener on Deno or Node and rejecting every listen on unsupported
platforms before any bind. The full wire specification lives in docs/diagnostics-protocol.md.