{
  "docs/api/Sonde.EvalOptions.html": {
    "href": "docs/api/Sonde.EvalOptions.html",
    "title": "Class EvalOptions | Sonde",
    "summary": "Class EvalOptions Namespace Sonde Assembly Sonde.dll Knobs for one eval. public sealed record EvalOptions : IEquatable<EvalOptions> Inheritance object EvalOptions Implements IEquatable<EvalOptions> Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.ReferenceEquals(object, object) object.ToString() Fields DefaultTimeoutMs The default and clamp range for TimeoutMs. public const int DefaultTimeoutMs = 10000 Field Value int MaxMaxDepth The deepest result walk accepted. public const int MaxMaxDepth = 32 Field Value int MaxTimeoutMs The longest timeout accepted. public const int MaxTimeoutMs = 300000 Field Value int MinTimeoutMs The shortest timeout accepted. public const int MinTimeoutMs = 100 Field Value int Properties MaxDepth How deep the result serialiser walks an object graph. public int MaxDepth { get; init; } Property Value int TimeoutMs How long to wait for the script before the probe answers TimedOut. public int TimeoutMs { get; init; } Property Value int"
  },
  "docs/api/Sonde.EvalOutcome.html": {
    "href": "docs/api/Sonde.EvalOutcome.html",
    "title": "Enum EvalOutcome | Sonde",
    "summary": "Enum EvalOutcome Namespace Sonde Assembly Sonde.dll How an eval ended. public enum EvalOutcome Fields Busy = 4 The probe is already running an eval; BusySinceUtc says since when. CompileFailed = 3 The script did not compile; Diagnostics says why. Completed = 0 The script returned; Result holds its value. ProtocolError = 6 The probe refused the request as malformed; Error says why. ScriptThrew = 1 The script threw; Exception holds the rendered exception. TimedOut = 2 The timeout tripped first. The script may still be running inside the target. Unreachable = 5 No probe answered at the endpoint; Error says what happened."
  },
  "docs/api/Sonde.EvalResult.html": {
    "href": "docs/api/Sonde.EvalResult.html",
    "title": "Class EvalResult | Sonde",
    "summary": "Class EvalResult Namespace Sonde Assembly Sonde.dll What came back from an eval, whichever way it ended. public sealed record EvalResult : IEquatable<EvalResult> Inheritance object EvalResult Implements IEquatable<EvalResult> Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.ReferenceEquals(object, object) object.ToString() Properties BusySinceUtc When the running eval started, when the probe was busy. public DateTimeOffset? BusySinceUtc { get; init; } Property Value DateTimeOffset? Diagnostics Compiler errors, when the script did not compile. public IReadOnlyList<ScriptDiagnostic> Diagnostics { get; init; } Property Value IReadOnlyList<ScriptDiagnostic> ElapsedMs Wall time inside the target, when the script ran. public long ElapsedMs { get; init; } Property Value long Error What went wrong, for Unreachable and ProtocolError. public string? Error { get; init; } Property Value string Exception The script's exception as text, when it threw. public string? Exception { get; init; } Property Value string LiveContexts Collectible load contexts the target still holds. public int LiveContexts { get; init; } Property Value int Logs Lines the script wrote with g.Log. public IReadOnlyList<string> Logs { get; init; } Property Value IReadOnlyList<string> Outcome How the eval ended. public EvalOutcome Outcome { get; init; } Property Value EvalOutcome Result The script's return value, when Outcome is Completed. public JsonNode? Result { get; init; } Property Value JsonNode"
  },
  "docs/api/Sonde.Launch.html": {
    "href": "docs/api/Sonde.Launch.html",
    "title": "Class Launch | Sonde",
    "summary": "Class Launch Namespace Sonde Assembly Sonde.dll How a process is launched so the probe is inside it: the environment variables, the endpoint naming, and the runtimeconfig template that reaches an app dotnet watch launches. Pure functions over dictionaries and paths; the caller starts the process. public static class Launch Inheritance object Launch Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.MemberwiseClone() object.ReferenceEquals(object, object) object.ToString() Fields EndpointVariable The variable the probe reads its endpoint from. public const string EndpointVariable = \"SONDE_ENDPOINT\" Field Value string HomeVariable Overrides where Sonde keeps sockets and templates (default ~/.sonde). public const string HomeVariable = \"SONDE_HOME\" Field Value string MaxSocketPathBytes The longest Unix domain socket path Linux accepts (sun_path is 108 bytes with its NUL). public const int MaxSocketPathBytes = 107 Field Value int StartupHooksVariable The runtime's list of startup-hook assemblies. public const string StartupHooksVariable = \"DOTNET_STARTUP_HOOKS\" Field Value string UserRuntimeConfigVariable The MSBuild property, read from the environment, naming a runtimeconfig template to merge in. public const string UserRuntimeConfigVariable = \"UserRuntimeConfig\" Field Value string Properties HomeDirectory Where sockets and templates live: $SONDE_HOME, else ~/.sonde. public static string HomeDirectory { get; } Property Value string Methods ApplyEnvironment(IDictionary<string, string?>, string, string?, string?) Adds the probe to a launch environment: DOTNET_STARTUP_HOOKS (appended with the platform path separator if one is already set), SONDE_ENDPOINT, and, when a runtimeconfig template was written, UserRuntimeConfig so a build the launch runs bakes the hook into the app's runtimeconfig too (see WriteRuntimeConfigTemplate(string, string, string?, string?)). public static void ApplyEnvironment(IDictionary<string, string?> environment, string endpoint, string? probeAssemblyPath = null, string? runtimeConfigTemplate = null) Parameters environment IDictionary<string, string> endpoint string probeAssemblyPath string runtimeConfigTemplate string EndpointFor(string) The endpoint for a process called name: on Windows a pipe name (sonde-{name}); elsewhere {home}/{name}.sock when that fits a socket path, else a hashed name in the same directory, else a hashed name under the temp directory. A deep home directory or a long name would otherwise give a path the probe cannot bind. public static string EndpointFor(string name) Parameters name string Returns string EndpointFor(string, string, string) The POSIX endpoint rule with its directories supplied, for tests. public static string EndpointFor(string name, string homeDirectory, string tempDirectory) Parameters name string homeDirectory string tempDirectory string Returns string WriteRuntimeConfigTemplate(string, string, string?, string?) Writes {directory}/{name}.runtimeconfig.template.json naming the probe in configProperties.STARTUP_HOOKS, merged over the project's own runtimeconfig.template.json when projectDirectory has one, and returns its path. dotnet watch run rewrites DOTNET_STARTUP_HOOKS for the app it launches, keeping only its own hooks, so an app under watch never sees the probe through the environment. The runtime merges this property with the environment's hooks, so this route reaches the app and leaves hot reload intact. MSBuild reads UserRuntimeConfig from the environment because the SDK only defaults it when empty. Returns null when the template cannot be written. public static string? WriteRuntimeConfigTemplate(string directory, string name, string? probeAssemblyPath = null, string? projectDirectory = null) Parameters directory string name string probeAssemblyPath string projectDirectory string Returns string"
  },
  "docs/api/Sonde.Probe.HookContext.html": {
    "href": "docs/api/Sonde.Probe.HookContext.html",
    "title": "Class HookContext | Sonde",
    "summary": "Class HookContext Namespace Sonde.Probe Assembly Sonde.Probe.dll What a hook's prefix or postfix sees and can change: the call's instance and arguments, and (for a non-void target) its result. A prefix that sets Skip short-circuits the original and returns Result; a postfix may rewrite Result. public sealed class HookContext Inheritance object HookContext Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.ReferenceEquals(object, object) object.ToString() Properties Args The call's arguments, in declaration order. public IReadOnlyList<object?> Args { get; } Property Value IReadOnlyList<object> Instance The target's this, or null for a static method. public object? Instance { get; } Property Value object Result The return value: what the original produced (in a postfix), or what to return instead. public object? Result { get; set; } Property Value object Skip Set by a prefix to skip the original and return Result. public bool Skip { get; set; } Property Value bool"
  },
  "docs/api/Sonde.Probe.HookHandle.html": {
    "href": "docs/api/Sonde.Probe.HookHandle.html",
    "title": "Class HookHandle | Sonde",
    "summary": "Class HookHandle Namespace Sonde.Probe Assembly Sonde.Probe.dll A live hook, returned by g.Hook(...) and passed to g.Unhook(...). public sealed class HookHandle Inheritance object HookHandle Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.ReferenceEquals(object, object) object.ToString() Properties Description The target method, as reported in Status. public string Description { get; } Property Value string Id Identifies the hook for g.Unhook. public Guid Id { get; } Property Value Guid"
  },
  "docs/api/Sonde.Probe.HookPatches.html": {
    "href": "docs/api/Sonde.Probe.HookPatches.html",
    "title": "Class HookPatches | Sonde",
    "summary": "Class HookPatches Namespace Sonde.Probe Assembly Sonde.Probe.dll The shared Harmony patch methods. Public because Harmony's generated code calls them; scripts never do. Chosen per target by whether it returns a value, so a void method is never given a __result (which Harmony rejects at patch time). [SuppressMessage(\"Naming\", \"CA1707:Identifiers should not contain underscores\", Justification = \"Harmony binds its injected parameters (__originalMethod, __args, __instance, __result) by exactly these names.\")] public static class HookPatches Inheritance object HookPatches Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.MemberwiseClone() object.ReferenceEquals(object, object) object.ToString() Methods PostfixNoResult(MethodBase, object?[], object?) Postfix for a void target. public static void PostfixNoResult(MethodBase __originalMethod, object?[] __args, object? __instance) Parameters __originalMethod MethodBase __args object[] __instance object PostfixWithResult(MethodBase, object?[], object?, ref object?) Postfix for a non-void target. public static void PostfixWithResult(MethodBase __originalMethod, object?[] __args, object? __instance, ref object? __result) Parameters __originalMethod MethodBase __args object[] __instance object __result object PrefixNoResult(MethodBase, object?[], object?) Prefix for a void target. public static bool PrefixNoResult(MethodBase __originalMethod, object?[] __args, object? __instance) Parameters __originalMethod MethodBase __args object[] __instance object Returns bool PrefixWithResult(MethodBase, object?[], object?, ref object?) Prefix for a non-void target. public static bool PrefixWithResult(MethodBase __originalMethod, object?[] __args, object? __instance, ref object? __result) Parameters __originalMethod MethodBase __args object[] __instance object __result object Returns bool"
  },
  "docs/api/Sonde.Probe.Protocol.Framing.html": {
    "href": "docs/api/Sonde.Probe.Protocol.Framing.html",
    "title": "Class Framing | Sonde",
    "summary": "Class Framing Namespace Sonde.Probe.Protocol Assembly Sonde.Probe.dll The wire framing: a 4-byte little-endian payload length, then the UTF-8 JSON body. One request and one response per connection. Works on any Stream, so the same code serves a socket, a pipe, or a MemoryStream in a test. The spec is docs/protocol.md. public static class Framing Inheritance object Framing Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.MemberwiseClone() object.ReferenceEquals(object, object) object.ToString() Fields MaxFrameBytes Hard cap on a single frame. Compiled scripts are a few hundred KB at most. public const int MaxFrameBytes = 134217728 Field Value int Properties Json The JSON shape on the wire: camelCase names, enums as camelCase strings, nulls omitted. public static JsonSerializerOptions Json { get; } Property Value JsonSerializerOptions Methods ReadFrameAsync(Stream, CancellationToken) Reads one frame. Returns null on a clean end of stream before any header byte. public static Task<byte[]?> ReadFrameAsync(Stream stream, CancellationToken cancellationToken = default) Parameters stream Stream cancellationToken CancellationToken Returns Task<byte[]> Exceptions InvalidDataException The header names a length outside 0..MaxFrameBytes. EndOfStreamException The stream ended inside the header or the body. ReadMessageAsync<T>(Stream, CancellationToken) Reads one frame and deserialises it as T. Default on a clean end of stream. public static Task<T?> ReadMessageAsync<T>(Stream stream, CancellationToken cancellationToken = default) Parameters stream Stream cancellationToken CancellationToken Returns Task<T> Type Parameters T WriteFrameAsync(Stream, ReadOnlyMemory<byte>, CancellationToken) Writes one frame: the length header, then the payload, then a flush. public static Task WriteFrameAsync(Stream stream, ReadOnlyMemory<byte> payload, CancellationToken cancellationToken = default) Parameters stream Stream payload ReadOnlyMemory<byte> cancellationToken CancellationToken Returns Task Exceptions InvalidOperationException The payload exceeds MaxFrameBytes. WriteMessageAsync<T>(Stream, T, CancellationToken) Serialises message with Json and writes it as one frame. public static Task WriteMessageAsync<T>(Stream stream, T message, CancellationToken cancellationToken = default) Parameters stream Stream message T cancellationToken CancellationToken Returns Task Type Parameters T"
  },
  "docs/api/Sonde.Probe.Protocol.HelloResponse.html": {
    "href": "docs/api/Sonde.Probe.Protocol.HelloResponse.html",
    "title": "Class HelloResponse | Sonde",
    "summary": "Class HelloResponse Namespace Sonde.Probe.Protocol Assembly Sonde.Probe.dll The answer to a Hello. public sealed record HelloResponse : IEquatable<HelloResponse> Inheritance object HelloResponse Implements IEquatable<HelloResponse> Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.ReferenceEquals(object, object) object.ToString() Properties DiRootCaptured True once the probe has found the host's dependency-injection root. public bool DiRootCaptured { get; init; } Property Value bool EntryAssembly The host's entry assembly name, if the runtime reports one. public string? EntryAssembly { get; init; } Property Value string LiveContexts Collectible load contexts not yet unloaded (a timed-out script holds one open). public int LiveContexts { get; init; } Property Value int LoadedAssemblyNames Names of loaded assemblies that have no file path (a single-file bundle). A client resolves these from the shared framework directory. public IReadOnlyList<string> LoadedAssemblyNames { get; init; } Property Value IReadOnlyList<string> LoadedAssemblyPaths File paths of the host's loaded assemblies (dynamic and in-memory ones skipped). A client compiles scripts against these. public IReadOnlyList<string> LoadedAssemblyPaths { get; init; } Property Value IReadOnlyList<string> ProcessId The host's process id. public int ProcessId { get; init; } Property Value int RuntimeVersion The host's runtime, e.g. .NET 9.0.4. public string RuntimeVersion { get; init; } Property Value string SondeVersion The probe's own version, so a stale agent is visible. public string SondeVersion { get; init; } Property Value string"
  },
  "docs/api/Sonde.Probe.Protocol.ProbeRequest.html": {
    "href": "docs/api/Sonde.Probe.Protocol.ProbeRequest.html",
    "title": "Class ProbeRequest | Sonde",
    "summary": "Class ProbeRequest Namespace Sonde.Probe.Protocol Assembly Sonde.Probe.dll One request per connection. Kind selects which fields are read. public sealed record ProbeRequest : IEquatable<ProbeRequest> Inheritance object ProbeRequest Implements IEquatable<ProbeRequest> Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.ReferenceEquals(object, object) object.ToString() Properties AssemblyBase64 Run only: the compiled script assembly, base64-encoded. public string? AssemblyBase64 { get; init; } Property Value string EntryMethodName Run only: the static method to invoke on the entry type. public string EntryMethodName { get; init; } Property Value string EntryTypeName Run only: the script's entry type (the class the compiler wraps the script in). public string? EntryTypeName { get; init; } Property Value string Kind What to do. public ProbeRequestKind Kind { get; init; } Property Value ProbeRequestKind MaxDepth Run only: how deep the result serialiser walks an object graph. public int MaxDepth { get; init; } Property Value int TimeoutMs Run only: how long the probe waits for the script before answering with TimedOut. public int TimeoutMs { get; init; } Property Value int"
  },
  "docs/api/Sonde.Probe.Protocol.ProbeRequestKind.html": {
    "href": "docs/api/Sonde.Probe.Protocol.ProbeRequestKind.html",
    "title": "Enum ProbeRequestKind | Sonde",
    "summary": "Enum ProbeRequestKind Namespace Sonde.Probe.Protocol Assembly Sonde.Probe.dll What a request asks the probe to do. public enum ProbeRequestKind Fields Hello = 0 Describe the process: runtime, loaded assemblies, probe version. Run = 2 Load a compiled script assembly, run its entry method, return the result. Status = 1 Report the probe's state: running eval, live contexts, hooks and taps."
  },
  "docs/api/Sonde.Probe.Protocol.RunResponse.html": {
    "href": "docs/api/Sonde.Probe.Protocol.RunResponse.html",
    "title": "Class RunResponse | Sonde",
    "summary": "Class RunResponse Namespace Sonde.Probe.Protocol Assembly Sonde.Probe.dll The answer to a Run. public sealed record RunResponse : IEquatable<RunResponse> Inheritance object RunResponse Implements IEquatable<RunResponse> Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.ReferenceEquals(object, object) object.ToString() Properties BusySinceUtc Set when the request was refused because an eval is already running. One eval at a time. public DateTimeOffset? BusySinceUtc { get; init; } Property Value DateTimeOffset? ElapsedMs Wall time of the script. public long ElapsedMs { get; init; } Property Value long Exception The script's exception rendered as text (type, message, stack), or null. public string? Exception { get; init; } Property Value string LiveContexts Collectible load contexts not yet unloaded. public int LiveContexts { get; init; } Property Value int Logs Lines the script wrote with g.Log. public IReadOnlyList<string> Logs { get; init; } Property Value IReadOnlyList<string> ProtocolError A protocol-level error that is not a script exception: entry type not found, malformed request. public string? ProtocolError { get; init; } Property Value string Result The script's return value, serialised with depth and size caps. Null when the script returned null or nothing. public JsonNode? Result { get; init; } Property Value JsonNode TimedOut True when the timeout tripped before the script returned. The script's thread may still be running: a runaway eval cannot be killed. public bool TimedOut { get; init; } Property Value bool"
  },
  "docs/api/Sonde.Probe.Protocol.RunningEvalInfo.html": {
    "href": "docs/api/Sonde.Probe.Protocol.RunningEvalInfo.html",
    "title": "Class RunningEvalInfo | Sonde",
    "summary": "Class RunningEvalInfo Namespace Sonde.Probe.Protocol Assembly Sonde.Probe.dll The eval in progress, if any, on a StatusResponse. public sealed record RunningEvalInfo : IEquatable<RunningEvalInfo> Inheritance object RunningEvalInfo Implements IEquatable<RunningEvalInfo> Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.ReferenceEquals(object, object) object.ToString() Properties StartedAtUtc When the running eval started. public DateTimeOffset StartedAtUtc { get; init; } Property Value DateTimeOffset"
  },
  "docs/api/Sonde.Probe.Protocol.StatusResponse.html": {
    "href": "docs/api/Sonde.Probe.Protocol.StatusResponse.html",
    "title": "Class StatusResponse | Sonde",
    "summary": "Class StatusResponse Namespace Sonde.Probe.Protocol Assembly Sonde.Probe.dll The answer to a Status. public sealed record StatusResponse : IEquatable<StatusResponse> Inheritance object StatusResponse Implements IEquatable<StatusResponse> Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.ReferenceEquals(object, object) object.ToString() Properties DiRootCaptured True once the probe has found the host's dependency-injection root. public bool DiRootCaptured { get; init; } Property Value bool Hooks Descriptions of the active method hooks. public IReadOnlyList<string> Hooks { get; init; } Property Value IReadOnlyList<string> LiveContexts Collectible load contexts not yet unloaded. public int LiveContexts { get; init; } Property Value int ProcessId The host's process id. public int ProcessId { get; init; } Property Value int RunningEval The eval in progress, or null. public RunningEvalInfo? RunningEval { get; init; } Property Value RunningEvalInfo Taps Names of the DiagnosticListeners being tapped. public IReadOnlyList<string> Taps { get; init; } Property Value IReadOnlyList<string>"
  },
  "docs/api/Sonde.Probe.Protocol.html": {
    "href": "docs/api/Sonde.Probe.Protocol.html",
    "title": "Namespace Sonde.Probe.Protocol | Sonde",
    "summary": "Namespace Sonde.Probe.Protocol Classes Framing The wire framing: a 4-byte little-endian payload length, then the UTF-8 JSON body. One request and one response per connection. Works on any Stream, so the same code serves a socket, a pipe, or a MemoryStream in a test. The spec is docs/protocol.md. HelloResponse The answer to a Hello. ProbeRequest One request per connection. Kind selects which fields are read. RunResponse The answer to a Run. RunningEvalInfo The eval in progress, if any, on a StatusResponse. StatusResponse The answer to a Status. Enums ProbeRequestKind What a request asks the probe to do."
  },
  "docs/api/Sonde.Probe.ResultJson.html": {
    "href": "docs/api/Sonde.Probe.ResultJson.html",
    "title": "Class ResultJson | Sonde",
    "summary": "Class ResultJson Namespace Sonde.Probe Assembly Sonde.Probe.dll Serialises an arbitrary script return value to a JsonNode the client can always read back. Depth is capped, long strings and large collections are truncated with a marker, reference cycles are broken, and a type that refuses to serialise falls back to ToString(). The client must always get something back rather than a throw. public static class ResultJson Inheritance object ResultJson Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.MemberwiseClone() object.ReferenceEquals(object, object) object.ToString() Fields DefaultMaxDepth How deep an object graph is walked unless the request says otherwise. public const int DefaultMaxDepth = 6 Field Value int MaxCollectionItems Collections longer than this are cut with a marker. public const int MaxCollectionItems = 100 Field Value int MaxObjectMembers Objects with more public members than this are cut. public const int MaxObjectMembers = 200 Field Value int MaxStringLength Strings longer than this are cut with a marker saying how much was dropped. public const int MaxStringLength = 10000 Field Value int Methods Serialize(object?, int) Serialises value, walking at most maxDepth levels. public static JsonNode? Serialize(object? value, int maxDepth = 6) Parameters value object maxDepth int Returns JsonNode"
  },
  "docs/api/Sonde.Probe.ScriptGlobals.html": {
    "href": "docs/api/Sonde.Probe.ScriptGlobals.html",
    "title": "Class ScriptGlobals | Sonde",
    "summary": "Class ScriptGlobals Namespace Sonde.Probe Assembly Sonde.Probe.dll What a script sees as g. Constructed fresh per eval; Vars, hooks and taps are process-wide and persist across evals. The full surface, with examples, is docs/script-surface.md. [SuppressMessage(\"Performance\", \"CA1822:Mark members as static\", Justification = \"Scripts call these as g.Member; the instance is the script's whole surface.\")] [SuppressMessage(\"ApiDesign\", \"RS0026:Do not add multiple overloads with optional parameters\", Justification = \"Hook(MethodBase, ...) and Hook<T>(string, ...) differ in generic arity and first parameter type; a call can never be ambiguous between them.\")] public sealed class ScriptGlobals Inheritance object ScriptGlobals Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.ReferenceEquals(object, object) object.ToString() Properties Cancellation Trips at the eval timeout. A long-running script should honour it; a runaway one cannot be stopped from outside. public CancellationToken Cancellation { get; } Property Value CancellationToken Services The host's root service provider. public IServiceProvider Services { get; } Property Value IServiceProvider Vars Persists for the life of the process, across evals. public ConcurrentDictionary<string, object?> Vars { get; } Property Value ConcurrentDictionary<string, object> Methods Hook(MethodBase, Action<HookContext>?, Action<HookContext>?) Hooks a method with a prefix and/or postfix. Lives until unhooked or the process ends. Logs a warning when the target is small and non-virtual, because calls the JIT already inlined it into are not intercepted. public HookHandle Hook(MethodBase target, Action<HookContext>? prefix = null, Action<HookContext>? postfix = null) Parameters target MethodBase prefix Action<HookContext> postfix Action<HookContext> Returns HookHandle Hook<T>(string, Action<HookContext>?, Action<HookContext>?) Hooks T's method by name (public or non-public, instance or static). public HookHandle Hook<T>(string methodName, Action<HookContext>? prefix = null, Action<HookContext>? postfix = null) Parameters methodName string prefix Action<HookContext> postfix Action<HookContext> Returns HookHandle Type Parameters T Exceptions MissingMethodException No method of that name on T. Log(string) Appends a line returned alongside the result. public void Log(string message) Parameters message string Resolve<T>() Resolves a required service from the root provider. public T Resolve<T>() where T : notnull Returns T Type Parameters T Scope() A fresh disposable scope. Dispose it when done. public IServiceScope Scope() Returns IServiceScope Tap(string, int) Ring-buffers a named DiagnosticListener's events (e.g. Microsoft.EntityFrameworkCore, HttpHandlerDiagnosticListener). Persists across evals; read it with Tapped(string, DateTimeOffset?). public void Tap(string listenerName, int capacity = 1000) Parameters listenerName string capacity int Tapped(string, DateTimeOffset?) The captured events for a listener, optionally only those at or after since. public IReadOnlyList<TapEntry> Tapped(string listenerName, DateTimeOffset? since = null) Parameters listenerName string since DateTimeOffset? Returns IReadOnlyList<TapEntry> Unhook(HookHandle) Removes one hook. public void Unhook(HookHandle handle) Parameters handle HookHandle UnhookAll() Removes every hook in the process. public void UnhookAll() Untap(string) Stops tapping a listener and drops its buffer. public void Untap(string listenerName) Parameters listenerName string"
  },
  "docs/api/Sonde.Probe.SondeHost.html": {
    "href": "docs/api/Sonde.Probe.SondeHost.html",
    "title": "Class SondeHost | Sonde",
    "summary": "Class SondeHost Namespace Sonde.Probe Assembly Sonde.Probe.dll Process-wide probe state: the captured dependency-injection root, the cross-eval Vars bag, the live collectible-context count, and the endpoint. The startup hook populates it. A host that builds its own provider may hand it over with Register(IServiceProvider); that is the one line a host needs when automatic capture cannot find the root. public static class SondeHost Inheritance object SondeHost Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.MemberwiseClone() object.ReferenceEquals(object, object) object.ToString() Properties DiRootCaptured True once a root provider has been registered or captured. public static bool DiRootCaptured { get; } Property Value bool Endpoint The endpoint this process's probe listens on, recorded by the startup hook before it scrubs the variable from the environment. Null when the hook did not run. A host reads this, not the environment, to reach its own probe. public static string? Endpoint { get; } Property Value string IsActive True once the startup hook has initialised and its listener is starting. A host that merely references this assembly without the hook leaves this false, so Register(IServiceProvider) is a safe no-op there. public static bool IsActive { get; } Property Value bool LiveContexts Collectible load contexts not yet unloaded. A timed-out script holds one open until it ends. public static int LiveContexts { get; } Property Value int Services The root provider for scripts. An explicit root wins; otherwise a long-lived scope off the scope factory captured from the first HTTP request. public static IServiceProvider Services { get; } Property Value IServiceProvider Exceptions InvalidOperationException No root has been registered or captured yet. Vars Survives across evals for the life of the process: assign in one script, read in the next. public static ConcurrentDictionary<string, object?> Vars { get; } Property Value ConcurrentDictionary<string, object> Methods CreateScope() A fresh disposable scope for a script that wants scoped services in isolation. public static IServiceScope CreateScope() Returns IServiceScope Exceptions InvalidOperationException No root has been registered or captured yet. Register(IServiceProvider) Hands the probe the host's root provider. First writer wins; later calls, and calls when the probe is not active, are harmless no-ops. Call it right after the host is built: SondeHost.Register(app.Services). public static void Register(IServiceProvider services) Parameters services IServiceProvider"
  },
  "docs/api/Sonde.Probe.TapEntry.html": {
    "href": "docs/api/Sonde.Probe.TapEntry.html",
    "title": "Class TapEntry | Sonde",
    "summary": "Class TapEntry Namespace Sonde.Probe Assembly Sonde.Probe.dll One captured DiagnosticListener event: when, its name, and a depth-2 summary of its payload. public sealed record TapEntry : IEquatable<TapEntry> Inheritance object TapEntry Implements IEquatable<TapEntry> Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.ReferenceEquals(object, object) object.ToString() Constructors TapEntry(DateTimeOffset, string, JsonNode?) One captured DiagnosticListener event: when, its name, and a depth-2 summary of its payload. public TapEntry(DateTimeOffset Timestamp, string EventName, JsonNode? Payload) Parameters Timestamp DateTimeOffset When the event was observed. EventName string The event's name as the listener wrote it. Payload JsonNode The event's payload, serialised two levels deep. Properties EventName The event's name as the listener wrote it. public string EventName { get; init; } Property Value string Payload The event's payload, serialised two levels deep. public JsonNode? Payload { get; init; } Property Value JsonNode Timestamp When the event was observed. public DateTimeOffset Timestamp { get; init; } Property Value DateTimeOffset"
  },
  "docs/api/Sonde.Probe.html": {
    "href": "docs/api/Sonde.Probe.html",
    "title": "Namespace Sonde.Probe | Sonde",
    "summary": "Namespace Sonde.Probe Classes HookContext What a hook's prefix or postfix sees and can change: the call's instance and arguments, and (for a non-void target) its result. A prefix that sets Skip short-circuits the original and returns Result; a postfix may rewrite Result. HookHandle A live hook, returned by g.Hook(...) and passed to g.Unhook(...). HookPatches The shared Harmony patch methods. Public because Harmony's generated code calls them; scripts never do. Chosen per target by whether it returns a value, so a void method is never given a __result (which Harmony rejects at patch time). ResultJson Serialises an arbitrary script return value to a JsonNode the client can always read back. Depth is capped, long strings and large collections are truncated with a marker, reference cycles are broken, and a type that refuses to serialise falls back to ToString(). The client must always get something back rather than a throw. ScriptGlobals What a script sees as g. Constructed fresh per eval; Vars, hooks and taps are process-wide and persist across evals. The full surface, with examples, is docs/script-surface.md. SondeHost Process-wide probe state: the captured dependency-injection root, the cross-eval Vars bag, the live collectible-context count, and the endpoint. The startup hook populates it. A host that builds its own provider may hand it over with Register(IServiceProvider); that is the one line a host needs when automatic capture cannot find the root. TapEntry One captured DiagnosticListener event: when, its name, and a depth-2 summary of its payload."
  },
  "docs/api/Sonde.ProbeAssembly.html": {
    "href": "docs/api/Sonde.ProbeAssembly.html",
    "title": "Class ProbeAssembly | Sonde",
    "summary": "Class ProbeAssembly Namespace Sonde Assembly Sonde.dll Where Sonde.Probe.dll is: beside this library, which is where the package puts it. public static class ProbeAssembly Inheritance object ProbeAssembly Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.MemberwiseClone() object.ReferenceEquals(object, object) object.ToString() Fields FileName The file name the runtime loads. public const string FileName = \"Sonde.Probe.dll\" Field Value string Properties Path The probe's full path beside this library, or beside the application when this library is bundled. public static string Path { get; } Property Value string Exceptions FileNotFoundException The probe is not beside this library."
  },
  "docs/api/Sonde.ProbeClient.html": {
    "href": "docs/api/Sonde.ProbeClient.html",
    "title": "Class ProbeClient | Sonde",
    "summary": "Class ProbeClient Namespace Sonde Assembly Sonde.dll The client side of the wire protocol: one connection per request, a Unix domain socket on POSIX, a named pipe on Windows. Compilation and result shaping live above this in SondeClient; this only moves frames. public static class ProbeClient Inheritance object ProbeClient Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.MemberwiseClone() object.ReferenceEquals(object, object) object.ToString() Methods HelloAsync(string, CancellationToken) Asks the probe at endpoint to describe its process. public static Task<HelloResponse?> HelloAsync(string endpoint, CancellationToken cancellationToken = default) Parameters endpoint string cancellationToken CancellationToken Returns Task<HelloResponse> IsListeningAsync(string, TimeSpan) True when something accepts a connection at endpoint within timeout. public static Task<bool> IsListeningAsync(string endpoint, TimeSpan timeout) Parameters endpoint string timeout TimeSpan Returns Task<bool> RunAsync(string, ProbeRequest, CancellationToken) Sends a compiled script to the probe at endpoint and waits for its answer. public static Task<RunResponse?> RunAsync(string endpoint, ProbeRequest request, CancellationToken cancellationToken = default) Parameters endpoint string request ProbeRequest cancellationToken CancellationToken Returns Task<RunResponse> StatusAsync(string, CancellationToken) Asks the probe at endpoint for its state. public static Task<StatusResponse?> StatusAsync(string endpoint, CancellationToken cancellationToken = default) Parameters endpoint string cancellationToken CancellationToken Returns Task<StatusResponse>"
  },
  "docs/api/Sonde.ScriptCompileResult.html": {
    "href": "docs/api/Sonde.ScriptCompileResult.html",
    "title": "Class ScriptCompileResult | Sonde",
    "summary": "Class ScriptCompileResult Namespace Sonde Assembly Sonde.dll What Compile(string, IEnumerable<string>, IEnumerable<string>, string) produced: an assembly, or the errors. public sealed record ScriptCompileResult : IEquatable<ScriptCompileResult> Inheritance object ScriptCompileResult Implements IEquatable<ScriptCompileResult> Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.ReferenceEquals(object, object) object.ToString() Properties Assembly The compiled assembly's bytes, when successful. public ReadOnlyMemory<byte> Assembly { get; } Property Value ReadOnlyMemory<byte> Diagnostics The errors, when not successful. public IReadOnlyList<ScriptDiagnostic> Diagnostics { get; } Property Value IReadOnlyList<ScriptDiagnostic> Success True when Assembly is set. public bool Success { get; } Property Value bool"
  },
  "docs/api/Sonde.ScriptCompiler.html": {
    "href": "docs/api/Sonde.ScriptCompiler.html",
    "title": "Class ScriptCompiler | Sonde",
    "summary": "Class ScriptCompiler Namespace Sonde Assembly Sonde.dll Compiles a script against a target's loaded assemblies (and their siblings on disk) plus Sonde.Probe.dll, into an in-memory assembly whose entry type the probe invokes. The script is the body of Run(ScriptGlobals g, CancellationToken ct): statements ending in return x;. A #line directive makes diagnostics report the script's own line numbers. public static class ScriptCompiler Inheritance object ScriptCompiler Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.MemberwiseClone() object.ReferenceEquals(object, object) object.ToString() Fields EntryMethodName The method on EntryTypeName the probe invokes. public const string EntryMethodName = \"Run\" Field Value string EntryTypeName The type the script body is wrapped in; the probe looks it up by this name. public const string EntryTypeName = \"SondeScript\" Field Value string MaxSiblingsPerDirectory A directory with more managed files than this is not a framework or an app's bin, and is not scanned. public const int MaxSiblingsPerDirectory = 2000 Field Value int Properties CachedReferenceCount How many files the reference cache currently holds. Diagnostic. public static int CachedReferenceCount { get; } Property Value int Methods Compile(string, IEnumerable<string>, IEnumerable<string>, string) Compiles script against referencePaths (the target's loaded assembly files, plus every managed sibling in their directories), referenceNames (assemblies the target reported without a path, resolved from the runtime directory) and the probe assembly. public static ScriptCompileResult Compile(string script, IEnumerable<string> referencePaths, IEnumerable<string> referenceNames, string probeAssemblyPath) Parameters script string referencePaths IEnumerable<string> referenceNames IEnumerable<string> probeAssemblyPath string Returns ScriptCompileResult IsManagedAssembly(string) True when path is a PE file with CLI metadata. public static bool IsManagedAssembly(string path) Parameters path string Returns bool"
  },
  "docs/api/Sonde.ScriptDiagnostic.html": {
    "href": "docs/api/Sonde.ScriptDiagnostic.html",
    "title": "Class ScriptDiagnostic | Sonde",
    "summary": "Class ScriptDiagnostic Namespace Sonde Assembly Sonde.dll One compiler error, positioned in the script's own lines. public sealed record ScriptDiagnostic : IEquatable<ScriptDiagnostic> Inheritance object ScriptDiagnostic Implements IEquatable<ScriptDiagnostic> Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.ReferenceEquals(object, object) object.ToString() Constructors ScriptDiagnostic(int, int, string, string) One compiler error, positioned in the script's own lines. public ScriptDiagnostic(int Line, int Column, string Id, string Message) Parameters Line int 1-based line in the script; 0 when the error has no position. Column int 1-based column; 0 when the error has no position. Id string The Roslyn diagnostic id, e.g. CS0103. Message string The compiler's message. Properties Column 1-based column; 0 when the error has no position. public int Column { get; init; } Property Value int Id The Roslyn diagnostic id, e.g. CS0103. public string Id { get; init; } Property Value string Line 1-based line in the script; 0 when the error has no position. public int Line { get; init; } Property Value int Message The compiler's message. public string Message { get; init; } Property Value string"
  },
  "docs/api/Sonde.SondeClient.html": {
    "href": "docs/api/Sonde.SondeClient.html",
    "title": "Class SondeClient | Sonde",
    "summary": "Class SondeClient Namespace Sonde Assembly Sonde.dll A client for one probe: hello, status, and the whole eval flow (hello, compile against what the target has loaded, run, shape the answer). Stateless beyond the endpoint; create one per target. public sealed class SondeClient Inheritance object SondeClient Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.ReferenceEquals(object, object) object.ToString() Constructors SondeClient(string) A client for the probe at endpoint (a socket path or a pipe name). public SondeClient(string endpoint) Parameters endpoint string SondeClient(string, string?) A client that compiles against a specific probe assembly rather than the one beside this library. public SondeClient(string endpoint, string? probeAssemblyPath) Parameters endpoint string probeAssemblyPath string Properties Endpoint The endpoint this client talks to. public string Endpoint { get; } Property Value string Methods EvalAsync(string, EvalOptions?, CancellationToken) Compiles script against the target and runs it. Never throws for an outcome the script or the target produced; those are in the result. public Task<EvalResult> EvalAsync(string script, EvalOptions? options = null, CancellationToken cancellationToken = default) Parameters script string options EvalOptions cancellationToken CancellationToken Returns Task<EvalResult> HelloAsync(CancellationToken) Describes the target: runtime, process id, loaded assemblies, probe version. public Task<HelloResponse> HelloAsync(CancellationToken cancellationToken = default) Parameters cancellationToken CancellationToken Returns Task<HelloResponse> Exceptions SondeUnreachableException No probe answered. StatusAsync(CancellationToken) Reports the probe's state: running eval, live contexts, hooks and taps. public Task<StatusResponse> StatusAsync(CancellationToken cancellationToken = default) Parameters cancellationToken CancellationToken Returns Task<StatusResponse> Exceptions SondeUnreachableException No probe answered."
  },
  "docs/api/Sonde.SondeUnreachableException.html": {
    "href": "docs/api/Sonde.SondeUnreachableException.html",
    "title": "Class SondeUnreachableException | Sonde",
    "summary": "Class SondeUnreachableException Namespace Sonde Assembly Sonde.dll No probe answered at an endpoint. public sealed class SondeUnreachableException : Exception, ISerializable Inheritance object Exception SondeUnreachableException Implements ISerializable Inherited Members Exception.GetBaseException() Exception.GetType() Exception.ToString() Exception.Data Exception.HelpLink Exception.HResult Exception.InnerException Exception.Message Exception.Source Exception.StackTrace Exception.TargetSite object.Equals(object) object.Equals(object, object) object.GetHashCode() object.ReferenceEquals(object, object) Constructors SondeUnreachableException() Creates the exception. public SondeUnreachableException() SondeUnreachableException(string) Creates the exception with a message. public SondeUnreachableException(string message) Parameters message string SondeUnreachableException(string, Exception?) Creates the exception with a message and a cause. public SondeUnreachableException(string message, Exception? innerException) Parameters message string innerException Exception SondeUnreachableException(string, string, Exception?) Creates the exception for endpoint. public SondeUnreachableException(string endpoint, string reason, Exception? innerException) Parameters endpoint string reason string innerException Exception Properties Endpoint The endpoint that did not answer. public string? Endpoint { get; } Property Value string"
  },
  "docs/api/Sonde.TargetInfo.html": {
    "href": "docs/api/Sonde.TargetInfo.html",
    "title": "Class TargetInfo | Sonde",
    "summary": "Class TargetInfo Namespace Sonde Assembly Sonde.dll An endpoint under the Sonde home directory and whether a probe answers there. public sealed record TargetInfo : IEquatable<TargetInfo> Inheritance object TargetInfo Implements IEquatable<TargetInfo> Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.ReferenceEquals(object, object) object.ToString() Constructors TargetInfo(string, string, bool) An endpoint under the Sonde home directory and whether a probe answers there. public TargetInfo(string Name, string Endpoint, bool Listening) Parameters Name string The name the process was launched under. Endpoint string Its socket path or pipe name. Listening bool True when a connect was accepted. Properties Endpoint Its socket path or pipe name. public string Endpoint { get; init; } Property Value string Listening True when a connect was accepted. public bool Listening { get; init; } Property Value bool Name The name the process was launched under. public string Name { get; init; } Property Value string"
  },
  "docs/api/Sonde.Targets.html": {
    "href": "docs/api/Sonde.Targets.html",
    "title": "Class Targets | Sonde",
    "summary": "Class Targets Namespace Sonde Assembly Sonde.dll Turns what a person or agent typed into an endpoint, and lists the endpoints that answer. public static class Targets Inheritance object Targets Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.MemberwiseClone() object.ReferenceEquals(object, object) object.ToString() Methods ListAsync(CancellationToken) Every endpoint under HomeDirectory, with whether a probe answers at each. public static Task<IReadOnlyList<TargetInfo>> ListAsync(CancellationToken cancellationToken = default) Parameters cancellationToken CancellationToken Returns Task<IReadOnlyList<TargetInfo>> Resolve(string) A socket path or pipe name as given; otherwise the endpoint for a process of that name (see EndpointFor(string)). public static string Resolve(string target) Parameters target string Returns string"
  },
  "docs/api/Sonde.html": {
    "href": "docs/api/Sonde.html",
    "title": "Namespace Sonde | Sonde",
    "summary": "Namespace Sonde Classes EvalOptions Knobs for one eval. EvalResult What came back from an eval, whichever way it ended. Launch How a process is launched so the probe is inside it: the environment variables, the endpoint naming, and the runtimeconfig template that reaches an app dotnet watch launches. Pure functions over dictionaries and paths; the caller starts the process. ProbeAssembly Where Sonde.Probe.dll is: beside this library, which is where the package puts it. ProbeClient The client side of the wire protocol: one connection per request, a Unix domain socket on POSIX, a named pipe on Windows. Compilation and result shaping live above this in SondeClient; this only moves frames. ScriptCompileResult What Compile(string, IEnumerable<string>, IEnumerable<string>, string) produced: an assembly, or the errors. ScriptCompiler Compiles a script against a target's loaded assemblies (and their siblings on disk) plus Sonde.Probe.dll, into an in-memory assembly whose entry type the probe invokes. The script is the body of Run(ScriptGlobals g, CancellationToken ct): statements ending in return x;. A #line directive makes diagnostics report the script's own line numbers. ScriptDiagnostic One compiler error, positioned in the script's own lines. SondeClient A client for one probe: hello, status, and the whole eval flow (hello, compile against what the target has loaded, run, shape the answer). Stateless beyond the endpoint; create one per target. SondeUnreachableException No probe answered at an endpoint. TargetInfo An endpoint under the Sonde home directory and whether a probe answers there. Targets Turns what a person or agent typed into an endpoint, and lists the endpoints that answer. Enums EvalOutcome How an eval ended."
  },
  "docs/architecture.html": {
    "href": "docs/architecture.html",
    "title": "Architecture | Sonde",
    "summary": "Architecture Sonde is two halves and a wire between them. your shell / an agent the target process ┌───────────────────────────┐ ┌──────────────────────────────┐ │ sonde CLI · sonde-mcp │ │ Sonde.Probe (startup hook) │ │ └ Sonde (client lib) │ local socket/pipe │ ├ listener thread │ │ ├ ScriptCompiler ───┼── run(assembly) ────▶│ ├ EvalRunner (collectible │ │ │ (Roslyn) │◀── result JSON ──────┼───│ AssemblyLoadContext) │ │ └ ProbeClient │ │ ├ ScriptGlobals g.* │ └───────────────────────────┘ │ ├ Hooks (Harmony) │ │ └ Taps (DiagnosticListener)│ └──────────────────────────────┘ The probe is a small assembly the .NET runtime loads into the target before Main because DOTNET_STARTUP_HOOKS names it. It opens a local socket, waits, and runs whatever compiled script arrives against the process's real objects. It carries no compiler. The client compiles the script out of process with Roslyn, against the exact assemblies the target reports it has loaded, and sends the bytes over. Sixteen services on one box share one compiler instead of carrying 60 MB of Roslyn each. What the CLR allows, and what it does not This shape is forced by the runtime, not chosen for taste. Mechanism Needs opt-in at start? Runs code? Notes Startup hook (DOTNET_STARTUP_HOOKS) An environment variable only; no source change Yes What dotnet watch hot reload uses. This is Sonde. Roslyn scripting in-process A source change and 60-100 MB of Roslyn per host Yes Simplest, heaviest. Harmony / MonoMod detours No, but needs a way to run code first n/a Prefix/postfix any method. Misses call sites the JIT already inlined. Sonde's g.Hook. DiagnosticListener / EventListener No Observe only EF Core SQL, HttpClient, ASP.NET requests, no patching. Sonde's g.Tap. Hot reload (MetadataUpdater.ApplyUpdate) An environment variable Edits method bodies Needs the baseline compilation; a project of its own. Out of scope. ClrMD / dotnet-dump No No Read-only heap and stacks. Good for a process that did not opt in. Profiler attach (ICorProfiler) No Native, then managed The only true no-restart injection. Needs a native shim per platform. Out of scope. Debugger func-eval No Expressions only, process paused No lambdas, no new types. So: a process must start with the probe in it. There is no attach-time managed code injection into a CLR that did not opt in, short of writing a native profiler. Sonde says so plainly and does not chase it. After that one start, the process never restarts for debugging again. The startup hook StartupHook.Initialize() runs before the host's Main, on the main thread, and a throw there kills the host before it logs anything. Everything the hook does is inside one try, and the decision it makes is a pure function (Bootstrap.Plan) of two inputs: The entry assembly's path. If it is under an SDK version directory (…/sdk/9.0.312/dotnet.dll, dotnet-watch, MSBuild.dll, VBCSCompiler.dll), this process is tooling that will launch the real app as a child. The hook does nothing and leaves the environment alone so the child gets the probe. SONDE_ENDPOINT. Unset: the hook scrubs the environment and returns. No thread, no socket, no cost. Set: the hook records the endpoint, scrubs the environment, and starts. Scrubbing the environment means removing DOTNET_STARTUP_HOOKS and SONDE_ENDPOINT from the process's own environment block as the first thing after the decision. The runtime has already read them. Any child the host later starts, a dotnet build from a dev server, a helper process, a test host, would otherwise inherit them, load the probe, and fight for the same socket. The host reads its own endpoint from SondeHost.Endpoint, never from the environment. Starting registers a resolver so 0Harmony.dll loads from the probe's own directory, wires the DI-root capture, and starts one background thread named sonde-listener that sleeps 250 ms (letting the host settle) and then binds. If the endpoint's file exists and something still answers on it, the probe logs [Sonde] another probe already listens and stays dormant rather than stealing it; only a stale file left by a dead process is replaced. Delivering the hook under dotnet watch dotnet watch run rewrites DOTNET_STARTUP_HOOKS for the app it launches, keeping only its own hot-reload hook, so the environment route alone never reaches an app under watch. The runtime also reads startup hooks from the app's runtimeconfig.json (configProperties.STARTUP_HOOKS) and merges both lists. Sonde therefore writes a runtimeconfig.template.json naming the probe and points the build at it through the UserRuntimeConfig MSBuild property, which MSBuild reads from the environment. sonde env and sonde run set all three variables, so both routes are live at once, and the end-to-end suite proves each on its own. Finding the DI root A startup hook runs before the host has built anything, so there is no IServiceProvider to capture yet. Three paths, first one wins: A Harmony postfix on ServiceProvider's constructor records the first container the host builds. Works for web hosts and plain generic hosts alike (the Worker sample proves it). The first ASP.NET request. A DiagnosticListener observer takes IServiceScopeFactory from the request's RequestServices. The fallback if the patch could not be applied. SondeHost.Register(app.Services) from the host's own code. One line, for the host that wants to be explicit or whose container is not the standard one. Until one of them fires, g.Services throws with a message naming all three. Running a script The compiler wraps the script as the body of public static async Task<object?> Run(ScriptGlobals g, CancellationToken ct) inside a class named SondeScript, with a #line directive so errors report the script's own lines. It references every assembly the target reported, every managed sibling in those directories (so a script can name a framework assembly the host has not loaded yet), and Sonde.Probe.dll for ScriptGlobals. References are cached by path, size and mtime. The probe loads the assembly into a collectible AssemblyLoadContext whose resolver answers every reference with the host's already-loaded instance, so a type in the script is the same Type the host uses. It invokes Run on a thread-pool thread and waits up to the timeout. On return it serialises the result (before unloading, since the value may be of a type defined in the script), unloads the context, and answers. One eval at a time. A second run while one is in flight is refused with busySinceUtc. A runaway script cannot be stopped. .NET Core has no Thread.Abort. A script that ignores its cancellation token holds its thread, its load context and the eval slot until it finishes or the host restarts. The probe reports it under status.runningEval and liveContexts, and the docs say so wherever a script is written. Hooks g.Hook patches a method with Harmony. One Harmony patch per target method dispatches to every handle registered on it; the last handle removed unpatches. The patch methods are chosen by whether the target returns a value, because Harmony rejects a __result parameter on a void target. Harmony 2.4.2 or later is required: earlier releases throw on .NET 9 or refuse .NET 10. A small non-virtual target the JIT already inlined into its callers is not intercepted there; g.Hook warns when the target's IL is tiny and non-virtual. Taps g.Tap subscribes to DiagnosticListener.AllListeners, which replays every existing listener to a new subscriber, so it catches a listener created before or after the tap. Events are kept in a ring buffer with a two-level JSON summary of each payload. What is where Project Role Depends on Sonde.Probe The agent The shared framework, Lib.Harmony, the DI abstractions (compile-time only; see ADR 0002) Sonde Compiler, client, launch helpers, SondeClient Sonde.Probe, Microsoft.CodeAnalysis.CSharp Sonde.Cli The sonde tool Sonde, System.CommandLine Sonde.Mcp The sonde-mcp server Sonde, ModelContextProtocol Decisions and their reasons are in decisions/."
  },
  "docs/decisions/0001-charter.html": {
    "href": "docs/decisions/0001-charter.html",
    "title": "ADR 0001: Charter | Sonde",
    "summary": "ADR 0001: Charter Status: accepted · Date: 2026-10-12 · Decided by: Spencer Rose What Sonde is Sonde runs C# inside a live .NET process. An agent or a person writes a script, Sonde compiles it, sends it into the process, runs it against the process's real objects — its DI container, its caches, its connections — and returns the result as JSON. The process never restarts for debugging after the one start that loads Sonde. A sonde is a probe sent into something to take readings. That is the whole product. Three things a script can do: Read and call. Resolve any service from the host's IServiceProvider, walk private state, call methods, return a projection. Hook. Prefix or postfix any method with Harmony, keep the hook across calls, remove it later. Tap. Ring-buffer a DiagnosticListener (EF Core SQL, HttpClient, ASP.NET requests) with no patching. Where it comes from Sonde is extracted from Loom (/home/slowdata/sroot/code/loom), where it shipped as Loom.Probe in October 2026 and was proven on the Loom API itself, on headless nodes, on .NET 9 and .NET 10 ASP.NET services, under dotnet watch, and on Windows named pipes. The Loom copy is the reference implementation until Sonde 0.1 ships; then Loom consumes Sonde from NuGet and keeps only its own routing, gating and audit. Reference material in the Loom repo (same node, read-only from here): tools/loom-probe/ — the in-process agent (startup hook, listener, collectible ALC runner, Harmony hooks, taps, result JSON), with its README and 38 tests in tools/loom-probe.tests/. src/loom.api/Services/ProcessEval/ — the out-of-process compiler (ScriptCompiler.cs), the client (ProbeClient.cs), launch wiring (ProbeLaunch.cs: env, runtimeconfig template for dotnet watch), status cache. docs/responses/dotnet-eval-tunnel.md — the design survey (what the CLR allows and what it does not). docs/plans/2026-10-11-dotnet-eval-tunnel.md, 2026-10-11-probe-attach.md, 2026-10-12-probe-node-opt-out.md and docs/responses/*probe*-proofs.md — what was built, what broke, what was proven. Decisions already made Decision Choice Why Name Sonde Free on NuGet and GitHub (checked 2026-10-12). Short, says what it does. Licence MIT Lowest friction for contributors and for commercial hosts embedding it. Audience Agents first, people second The CLI and MCP server are the front door; a human REPL is a nice-to-have. Architecture In-process agent + out-of-process compiler Roslyn stays out of the target. Sixteen services share one compiler. Carried over from Loom, proven. Attach model Start with the hook; no live injection The CLR has no attach-time managed code injection short of a native profiler. We say so plainly and do not chase it. Targets .NET 8, 9, 10 on Linux, macOS, Windows The probe binds to the host's own framework at runtime; the CI matrix proves every pair. Transport Unix domain socket on POSIX, named pipe on Windows, length-prefixed JSON frames Local only, filesystem-permissioned. No network listener in the agent, ever. Security stance Sonde is remote code execution by design and has no authorisation of its own A local socket your user can open is the boundary. Hosts that route evals (Loom) add identity, gating and audit above it. SECURITY.md says this in the first paragraph. Repo shape Single repo, several packages Sonde.Probe (agent), Sonde (compiler + client library), sonde (dotnet tool CLI), Sonde.Mcp (MCP server). One version across all four. Non-negotiables carried from Loom's scars Each of these cost a day. They are requirements, not suggestions, and each gets a test. Scrub the environment at startup. The hook removes DOTNET_STARTUP_HOOKS and the endpoint variable from its own process so children never inherit the probe. Twelve listeners on one socket is what happens otherwise. Never steal a live socket. If something already answers at the endpoint, do not bind; log and stay dormant. Pass through SDK tooling. dotnet.dll, dotnet-watch, MSBuild workers: detect by entry assembly and do nothing. dotnet watch rewrites startup hooks for the app it launches. Deliver the hook through a runtimeconfig template plus the user-runtimeconfig variable as well, and test both routes. Dormant unless told. No endpoint variable means no listener, no thread, no cost. Nothing in the hook may throw. A startup hook that throws crashes the host before Main. Collectible AssemblyLoadContext per eval, unloaded after. Report live contexts. No Thread.Abort. A runaway script holds the eval slot until the host restarts. Refuse a second eval while one runs, pass a cancellation token, say so in the docs. Harmony 2.4.2 or later. 2.4.1 throws PlatformNotSupportedException on CoreCLR 10. Compile against the host's loaded assemblies and their siblings on disk. Framework assemblies the host has not loaded yet are still resolvable. Idle cost is a number in the README (Loom measured ~14 MB and one parked thread). Keep it measured in CI, not asserted. What \"gold standard\" means here A stranger should be able to go from the README to a working eval on their own app in five minutes, and from a fork to a merged PR in an afternoon without asking anyone anything. README with a 60-second demo (install tool → launch app with Sonde → run a script → JSON back), a GIF, the idle-cost number, and the attach caveat above the fold. docs/ with architecture, the wire protocol as a spec, the script surface, a security threat model, and an ADR per decision (this file is the first). CONTRIBUTING.md (how to build, test, run the samples, what a good PR looks like), a code of conduct, SECURITY.md with a disclosure route, issue and PR templates, CHANGELOG.md in Keep-a-Changelog form, SemVer. CI on GitHub Actions: build + test matrix across OS × .NET version, a real end-to-end job that launches a sample app and evals into it, package build on every PR, publish to NuGet on a tag. Reproducible builds, SourceLink, deterministic, symbols published. Analyzers on, warnings as errors, .editorconfig, nullable everywhere, public API tracked (PublicAPI.Shipped.txt). samples/ with a minimal web app and a worker service, each with a scripts/ folder of evals that double as the e2e tests. Every public type has XML docs; the docs site is generated from them. Out of scope for 1.0 Hot reload (MetadataUpdater.ApplyUpdate), profiler-based live attach, a web UI, any authentication or multi-user story, non-CoreCLR runtimes (Mono, NativeAOT). Still Spencer's to decide Where it lives on GitHub: srose-io/sonde (personal, simplest) or a new org (sonde-dev) so maintainers can be added without personal-account entanglement. Ask before the first push. Publish timing: push the repo public from the first commit, or build to 0.1 in private and publish with a working demo. Recommendation: private until the demo works, then public with a release. NuGet publishing account: whose API key signs releases."
  },
  "docs/decisions/0002-probe-dependencies.html": {
    "href": "docs/decisions/0002-probe-dependencies.html",
    "title": "ADR 0002: What the probe targets and depends on | Sonde",
    "summary": "ADR 0002: What the probe targets and depends on Status: accepted · Date: 2026-10-12 Context Sonde.Probe is loaded into other people's processes through DOTNET_STARTUP_HOOKS. It has to load into any supported host (.NET 8, 9 and 10; a web app or a worker service) without the host referencing it, and it must not drag a second copy of any assembly the host already has, or scripts see \"type X is not type X\". The reference implementation targeted net9.0 and took its dependency-injection types from the ASP.NET Core shared framework, which every host it was used on carried. A worker service does not carry that framework. Decision Target net8.0, the lowest supported host. A startup hook built for 8 loads into 9 and 10 hosts; one built for 9 does not load into an 8 host. The probe never needs anything newer than the 8 BCL. Reference Microsoft.Extensions.DependencyInjection.Abstractions with ExcludeAssets=\"runtime\". The probe compiles against IServiceProvider, IServiceScope and IServiceScopeFactory but ships no copy of the assembly. At runtime it binds to whichever copy the host loaded: the ASP.NET shared framework's in a web app, the app-local NuGet copy in a worker service. A host with no DI at all never resolves the types, and every member that would touch them is reached only by a script that asks for g.Services. Ship Harmony's own per-runtime builds with the probe under harmony/net8.0/, harmony/net9.0/ and harmony/net10.0/ beside Sonde.Probe.dll, and pick the host's at load time (HarmonyLocator: the host's major, else the newest below it). The Lib.Harmony package reference is private to the probe (PrivateAssets=\"all\"), so nothing about Harmony flows to a consumer through NuGet; a buildTransitive targets file copies the folder into the consumer's output. The first CI run showed why: a net9.0 consumer (the CLI, the test suites) received the package's net9.0 Harmony beside the probe, and a .NET 8 host could not load it, so the hook logged initialize failed and the probe stayed dormant. The host never references Harmony; a Default load-context Resolving handler, registered from the probe's module initializer, answers only for 0Harmony and only when nothing else resolved it. Nothing else. A test (Probe_ShipsHarmonyPerRuntimeAndNothingElse) asserts the referenced assembly list is the shared framework, the DI abstractions and Harmony, and that the three Harmony builds are on disk under harmony/. Consequences The probe's own code is limited to the .NET 8 API surface. Fine: it is small and uses nothing new. The DI assembly version the probe compiled against (8.0.x) is older than the host's in a 9 or 10 host; the runtime unifies upward, which is the normal case for any .NET library. A host that uses no DI container (a bare console app) gets a probe that answers Hello, Status and scripts that do not touch g.Services; g.Services throws with a message naming SondeHost.Register."
  },
  "docs/decisions/0003-mcp-surface.html": {
    "href": "docs/decisions/0003-mcp-surface.html",
    "title": "ADR 0003: The MCP surface | Sonde",
    "summary": "ADR 0003: The MCP surface Status: accepted · Date: 2026-10-12 Context Agents are Sonde's first audience. The Model Context Protocol is how an agent discovers and calls tools, so Sonde.Mcp is a front door on a par with the CLI. The question is what shape the tools take: one tool per protocol request, or a few coarse ones; errors as MCP errors or as data. Decision Transport: stdio only, through the official ModelContextProtocol package (2.2.0 at the time of writing). A client runs sonde-mcp as a child process. No HTTP: Sonde has no authorisation of its own, and an HTTP listener would invite someone to put it on a network. Four tools, named with a sonde_ prefix so they sort together in a client's tool list: sonde_list_targets, sonde_hello, sonde_status, sonde_eval. They mirror the CLI verbs one to one, so a person reading the CLI docs understands the tools and vice versa. Every tool answers JSON text, the same shapes the CLI's --json prints. An eval that did not run (did not compile, no probe, busy) is an outcome in that JSON, not an MCP error: an agent reads the diagnostics and fixes its script rather than seeing a failed call. One resource, sonde://script-surface, serving docs/script-surface.md verbatim (embedded at build time), so the agent can read what g offers without leaving the protocol. Tool annotations: the three read tools are marked read-only and idempotent; sonde_eval is marked destructive, because it is. Server instructions carry the one-paragraph orientation an agent needs before its first call, including that a script cannot be stopped once started. Consequences Adding a protocol request means adding a CLI verb and a tool together, and a test for each. The MCP package's API has moved between releases; the version is pinned centrally, and the stdio round-trip test in tests/Sonde.Mcp.Tests is what catches a breaking bump."
  },
  "docs/index.html": {
    "href": "docs/index.html",
    "title": "Sonde | Sonde",
    "summary": "Sonde Run C# inside a live .NET process. Start here: The README: the sixty-second demo. Architecture: how the probe gets in, how scripts run, what the CLR allows. The script surface: everything g offers, with examples. The wire protocol: the frame format and the three requests, for any language. Threat model: what bounds a tool that is remote code execution by design. Decisions: the ADRs. API reference: generated from the XML docs of every public type."
  },
  "docs/protocol.html": {
    "href": "docs/protocol.html",
    "title": "The wire protocol | Sonde",
    "summary": "The wire protocol This is the contract between a Sonde probe (inside the target process) and any client. It is small enough to implement in an afternoon in any language; the .NET client is Sonde.ProbeClient and the shapes are Sonde.Probe.Protocol.*. Transport POSIX: a Unix domain socket at the path in SONDE_ENDPOINT. The probe creates the file; its permissions are the user's default (umask), which is the security boundary. See the threat model. Windows: a named pipe, \\\\.\\pipe\\<name>, where <name> is SONDE_ENDPOINT. The probe never listens on TCP, and never will. One request and one response per connection. The client connects, writes one frame, reads one frame, and closes. The probe closes its end after answering. There is no session, so a client may open as many connections as it likes, in any order, from any number of processes; the probe serialises evals itself (see Busy below). Framing A frame is a 4-byte little-endian signed 32-bit length, then exactly that many bytes of UTF-8 JSON. The length is of the body only. A body may not exceed 128 MiB (134217728 bytes); a probe that reads a larger length closes the connection. A Hello request on the wire (71 bytes of body): 47 00 00 00 length = 71 7b 22 6b 69 6e 64 22 3a 22 68 65 6c 6c 6f {\"kind\":\"hello 22 2c ... \",... {\"kind\":\"hello\",\"entryMethodName\":\"Run\",\"timeoutMs\":10000,\"maxDepth\":6} The three trailing fields are the request's defaults, which the .NET client always sends; a client in another language may omit them. The test Framing_IsFourByteLittleEndianLengthThenUtf8Json asserts these exact bytes. JSON conventions Property names are camelCase. Enums are camelCase strings (\"hello\", not 0). Null properties are omitted by the probe; a client should treat a missing property as null. Unknown properties are ignored by both sides, so fields can be added without a version bump. Request { \"kind\": \"hello\" | \"status\" | \"run\", \"assemblyBase64\": \"...\", // run only: the compiled script assembly \"entryTypeName\": \"SondeScript\", // run only: the type to find in it \"entryMethodName\": \"Run\", // run only, default \"Run\": the public static method to invoke \"timeoutMs\": 10000, // run only, default 10000 \"maxDepth\": 6 // run only, default 6: how deep the result is serialised } hello Describes the process, and tells a compiler what to compile against. { \"runtimeVersion\": \".NET 9.0.4\", \"processId\": 12345, \"entryAssembly\": \"MinimalWeb\", \"loadedAssemblyPaths\": [\"/usr/share/dotnet/shared/Microsoft.NETCore.App/9.0.4/System.Private.CoreLib.dll\", \"...\"], \"loadedAssemblyNames\": [], \"diRootCaptured\": true, \"liveContexts\": 0, \"sondeVersion\": \"0.1.0\" } Field Meaning loadedAssemblyPaths File paths of every non-dynamic assembly the process has loaded. Compile against these, and against their siblings on disk so the script can name a framework assembly the host has not touched yet. loadedAssemblyNames Names of loaded assemblies that have no file (a single-file bundle). Resolve them from the runtime directory. diRootCaptured Whether g.Services will work. liveContexts Collectible load contexts not yet unloaded. A script that timed out holds one until it ends. sondeVersion The probe's informational version, so a stale agent is visible. status { \"processId\": 12345, \"diRootCaptured\": true, \"liveContexts\": 0, \"runningEval\": { \"startedAtUtc\": \"2026-10-12T10:00:00Z\" }, // omitted when idle \"hooks\": [\"MinimalWeb.CounterService.Increment()\"], \"taps\": [\"Microsoft.EntityFrameworkCore\"] } run The client compiles the script to a .NET assembly whose entryTypeName has a public static entryMethodName with one of these signatures (the probe picks by parameter count): static object? Run() static object? Run(ScriptGlobals g) static object? Run(ScriptGlobals g, CancellationToken ct) // what the Sonde compiler emits A Task or Task<T> return value is awaited. The probe loads the assembly into a collectible AssemblyLoadContext, resolves its references to the host's already-loaded assemblies (so types are the host's own, not copies), invokes the method on a thread-pool thread, waits up to timeoutMs, serialises the return value, and unloads the context. { \"result\": { \"...\": \"...\" }, // the return value as JSON; omitted when null \"logs\": [\"a line from g.Log\"], \"exception\": \"System.InvalidOperationException: ...\\n at ...\", // when the script threw \"elapsedMs\": 12, \"timedOut\": false, \"liveContexts\": 0, \"busySinceUtc\": \"2026-10-12T10:00:00Z\", // when refused: an eval is already running \"protocolError\": \"Entry type 'X' not found in the compiled script.\" // when the request was malformed } Exactly one of these is true of a run response: Case What is set The script returned result (unless it returned null), logs, elapsedMs The script threw exception, logs The timeout tripped first timedOut: true, logs, elapsedMs. The script is still running inside the host. An eval was already running busySinceUtc. Nothing was loaded. The request was malformed protocolError. Nothing ran. Busy. One eval at a time per process. A second run while one is in flight is refused with busySinceUtc rather than queued, so a client always knows what is running. A script that timed out keeps the slot until its thread actually finishes, because there is no way to stop it; status shows it under runningEval. Result serialisation The return value becomes JSON by walking it: scalars (numbers, strings, bools, enums as strings, Guid, dates, TimeSpan, Uri, Version) directly; dictionaries as objects; other enumerables as arrays; everything else as an object of its public properties and fields. Caps: Cap Value Marker Depth maxDepth (default 6) \"<max-depth TypeName>\" String length 10 000 chars \"…(truncated N chars)\" appended Collection items 100 \"<truncated, +more items>\" as the last item; \"…\": \"(+N more)\" for dictionaries Object members 200 silently stops Reference cycle \"<cycle TypeName>\" A getter that threw \"<error: message>\" A value that will not serialise its ToString() A JsonNode, JsonElement or JsonDocument returned by a script passes through as data. Versioning The protocol is unversioned in 0.x. A field is only ever added, never renamed or removed; a client ignores what it does not know. A breaking change will add a protocolVersion to hello first."
  },
  "docs/script-surface.html": {
    "href": "docs/script-surface.html",
    "title": "The script surface | Sonde",
    "summary": "The script surface A Sonde script is the body of one method. Sonde wraps it as public static async Task<object?> Run(ScriptGlobals g, CancellationToken ct) { // your script } so it is statements ending in return x;. await works. The usings you get for free are System, System.Collections, System.Collections.Generic, System.Linq, System.Reflection, System.Text, System.Threading, System.Threading.Tasks and Sonde.Probe. Anything the host process could reference, the script can reference: Sonde compiles against every assembly the host has loaded and every managed sibling beside them on disk. The return value comes back as JSON: depth-capped (6 levels by default), long strings and large collections truncated with a marker, reference cycles broken, and ToString() as the fallback for a value that refuses to serialise. Return a projection (new { a.Id, a.Name }) rather than a whole object graph. The host's internals are not visible to the compiler. Use reflection for private state: var cache = g.Resolve<IMemoryCache>(); var entries = cache.GetType().GetField(\"_entries\", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(cache); return entries; g: what a script can reach Member What it does g.Services The host's root IServiceProvider. Throws with a hint if Sonde has not found it yet (see below). g.Resolve<T>() g.Services.GetRequiredService<T>(). g.Scope() A fresh IServiceScope. Dispose it (using var scope = g.Scope();). g.Vars A ConcurrentDictionary<string, object?> that lives for the life of the process. Assign in one eval, read in the next. g.Log(string) A line returned alongside the result. g.Cancellation Trips at the eval timeout. Also passed as ct. A script that ignores it cannot be stopped from outside. g.Hook(MethodBase, prefix, postfix) Intercepts a method (see Hooks). Returns a HookHandle. g.Hook<T>(\"Name\", prefix, postfix) The same, finding T's method by name, public or private, instance or static. g.Unhook(handle) / g.UnhookAll() Removes one hook, or every hook in the process. g.Tap(\"ListenerName\", capacity) Ring-buffers a DiagnosticListener's events (see Taps). g.Tapped(\"ListenerName\", since) The captured events, optionally only those after since. g.Untap(\"ListenerName\") Stops the tap and drops its buffer. Examples Read configuration: return g.Resolve<IConfiguration>()[\"Logging:LogLevel:Default\"]; Call a service and project the answer: var orders = g.Resolve<IOrderRepository>(); var late = await orders.FindLateAsync(ct); return late.Select(o => new { o.Id, o.CustomerName, DaysLate = (DateTime.UtcNow - o.Due).Days }); Keep state across evals: g.Vars[\"baseline\"] = GC.GetTotalMemory(false); return \"recorded\"; return GC.GetTotalMemory(false) - (long)g.Vars[\"baseline\"]!; Hooks g.Hook patches a method with Harmony so a prefix runs before it and a postfix after. The hook stays until g.Unhook, g.UnhookAll, or the process ends. The prefix and postfix receive a HookContext: Member Meaning Instance The target's this, or null for a static method. Args The call's arguments. Result In a postfix, what the method returned; set it to return something else. In a prefix, the value to return when Skip is set. Skip Set in a prefix to skip the original method. Watch every call for a while: var handle = g.Hook<PricingService>(\"Quote\", prefix: c => g.Vars[\"lastQuoteArgs\"] = c.Args.ToArray()); g.Vars[\"quoteHook\"] = handle; return handle.Description; Rewrite a result to reproduce a bug without touching source: g.Hook<FeatureFlags>(\"IsEnabled\", postfix: c => { if ((string)c.Args[0]! == \"new-checkout\") c.Result = true; }); return \"forced on\"; Remove it: g.Unhook((HookHandle)g.Vars[\"quoteHook\"]!); return g.UnhookAll; Two limits. A small non-virtual method the JIT already inlined into its callers is not intercepted at those call sites; g.Hook logs a warning when the target looks like one. And a hook runs inside the host on the host's threads: keep it short and never block. Taps g.Tap subscribes to a named DiagnosticListener and keeps its last capacity events (1000 by default) with a two-level summary of each payload. Reading never clears the buffer. Names you will want: Listener What it carries Microsoft.EntityFrameworkCore Every SQL command EF Core runs, with its text and parameters. HttpHandlerDiagnosticListener Every HttpClient request and response. Microsoft.AspNetCore Every inbound request. g.Tap(\"Microsoft.EntityFrameworkCore\"); return \"tapping\"; return g.Tapped(\"Microsoft.EntityFrameworkCore\", since: DateTimeOffset.UtcNow.AddMinutes(-2)) .Where(e => e.EventName.EndsWith(\"CommandExecuted\")) .Select(e => e.Payload?[\"Command\"]?[\"CommandText\"]); Finding the DI root Sonde finds the host's root IServiceProvider three ways, first one wins: a patch on the container's constructor when the host builds it; the first ASP.NET request's RequestServices; or the host calling SondeHost.Register(app.Services) after it is built. If g.Services throws \"No DI root captured yet\", send the app one request, or add that one line."
  },
  "docs/threat-model.html": {
    "href": "docs/threat-model.html",
    "title": "Threat model | Sonde",
    "summary": "Threat model Sonde is remote code execution by design, and has no authorisation of its own. A script sent to a probe runs inside the host process with every right that process has: its memory, its connections, its credentials, its filesystem access, its network. Nothing in Sonde checks who sent the script. This page is about what does bound that, and what you are responsible for. The boundary is the endpoint A probe only ever listens on a local endpoint: POSIX: a Unix domain socket file. Whoever can connect() to the file can run code in the process. The file is created with the process's umask, in ~/.sonde by default, so by default that is the user the process runs as. There is no TCP listener and no way to add one. Windows: a named pipe with the default security descriptor, which admits the same user and administrators. So the question \"who can run code in this process?\" has the answer \"whoever can open the endpoint\", which is the same set of people who could already attach a debugger, read the process's memory with dotnet-dump, or replace its binaries. Sonde does not widen that set. It does make the capability convenient, which is the point and also the risk. What you own: the endpoint's location and permissions. Keep SONDE_HOME on a filesystem with sane permissions. Do not put the socket in a world-writable directory with a predictable name. Do not bind-mount it into a container you do not trust. A probe only exists when you asked for one The hook does nothing unless SONDE_ENDPOINT is set in the process's environment at start. A process that merely has DOTNET_STARTUP_HOOKS pointing at the probe (because it inherited it) scrubs the variable and continues with no listener, no thread, no socket. That is tested. What you own: which processes you launch with the endpoint set. A production service should not be one of them unless you have decided that the people who can reach its endpoint should be able to run code in it. Children never inherit the probe The first thing the hook does is remove both variables from the process's own environment. A child process the host starts later (a build, a helper, a shell) does not get the probe, does not try to bind the same endpoint, and does not become a second door into the host. The host reads its endpoint from SondeHost.Endpoint. That is tested, in-process and end to end. A second process that is nonetheless given the same endpoint (a copy-pasted launch command) finds the socket answering and refuses to bind, logging [Sonde] another probe already listens. It does not unlink a live socket. Only a stale file nothing answers on is replaced. A script cannot be stopped .NET Core has no Thread.Abort. A script that ignores its cancellation token runs until it finishes or the host exits. The probe will not start a second eval while it runs, reports it in status, and counts its load context in liveContexts. A script that spins forever has pinned one thread-pool thread for the life of the process. What you own: honour ct in anything that might take a while, and treat sonde eval on a process you care about the way you would treat attaching a debugger to it. What a script can do that is worse than reading state Everything. A hook (g.Hook) rewrites what a method returns for every caller until unhooked. A script can call Environment.Exit, open sockets, write files, read secrets out of configuration. Sonde does not sandbox the script and could not: it runs as host code, by design. Hosting Sonde for other people If you route evals to processes on behalf of other people (a dashboard, a fleet tool, an agent platform), you are the authorisation layer. The reference host that Sonde was extracted from does three things above it, and so should you: Identity and gating. Only an authenticated administrator may send a script, and only to a process whose definition opted in. An audit trail. Every eval is written to a log with who sent it, when, to which process, and a hash (or the text) of the script. No network exposure of the endpoint. Your service talks to the probe over the local socket on the same machine, and exposes only its own authenticated API. SECURITY.md has the disclosure route. \"A script can do anything\" is the product, not a vulnerability; a way to reach a probe you should not be able to reach, or a probe that listens when it should be dormant, or the agent taking a host down, is."
  }
}