@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

ExportKind
createDiagnosticsClientfunction
DiagnosticsPluginfunction
DiagnosticsClientOptionsinterface
DiagnosticsPluginOptionsinterface
DiagnosticsSessionLifetimeinterface
IDiagnosticsClientinterface
IDiagnosticsPlugininterface

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).

ExportKindSince
DiagnosticsPluginfunction0.8.0
IDiagnosticsPlugininterface0.8.0
DiagnosticsPluginOptionsinterface0.8.0
createDiagnosticsClientfunction0.8.0
IDiagnosticsClientinterface0.8.0
DiagnosticsClientOptionsinterface0.8.0
DiagnosticsSessionLifetimeinterface0.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.

Agent card · TypeDoc API reference