Files
mxaccessgw/docs/WorkerBootstrap.md
T
Joseph Doherty ddad573b75 Merge origin/main with local pending work and update AGENTS.md references
- Resolve 14 conflicts from popping local stash on top of origin's
  eed1e88 + 8d3352f doc-comment additions (11 mechanical, plus
  version.rs, DashboardAuthenticatorTests.cs, DashboardGalaxyProjector.cs)
- Fix 4 test files that used AGENTS.md as the repo-root sentinel
  (now use CLAUDE.md, since AGENTS.md was removed in 4731ab5)
- Redirect 10 doc citations from AGENTS.md to the matching gateway.md
  sections (Value Model, Status Model, Security, STA Worker Thread
  Model, gRPC Layer rule, cancellation rule)

Verified: solution build clean, x86 worker build clean, 266/266
gateway tests passing, 121/121 worker tests passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 14:13:33 -04:00

224 lines
9.7 KiB
Markdown

# Worker Bootstrap
The bootstrap layer parses the command-line arguments and environment variables passed to the `MxGateway.Worker` process, validates them against the gateway contract, and produces either a populated `WorkerOptions` instance or a structured failure that maps to a `WorkerExitCode`.
## Overview
The worker process is a short-lived child of the gateway. The gateway side of this contract lives in [WorkerProcessLauncher](./WorkerProcessLauncher.md). On the worker side, `Program.cs` is a single line that delegates to `WorkerApplication.Run(args)`:
```csharp
using MxGateway.Worker;
return WorkerApplication.Run(args);
```
`WorkerApplication.Run` constructs the bootstrap dependencies (`EnvironmentVariableWorkerEnvironment`, `WorkerConsoleLogger` writing to `Console.Error`, and a `WorkerPipeClient`), runs `WorkerOptionsParser`, and routes the resulting `WorkerBootstrapResult` either into the pipe client or into a non-zero exit. Splitting parsing from process wiring lets tests substitute fakes for the environment, logger, and pipe client without spawning a child process.
## Worker Options
`WorkerOptions` is the validated input contract for a worker session. The gateway hands every field to the worker; the worker never reads configuration files.
```csharp
public sealed class WorkerOptions
{
public const string NonceEnvironmentVariableName = "MXGATEWAY_WORKER_NONCE";
public WorkerOptions(
string sessionId,
string pipeName,
uint protocolVersion,
string nonce)
{
SessionId = sessionId;
PipeName = pipeName;
ProtocolVersion = protocolVersion;
Nonce = nonce;
}
public string SessionId { get; }
public string PipeName { get; }
public uint ProtocolVersion { get; }
public string Nonce { get; }
}
```
### Required inputs
All four fields are required. Three arrive on the command line and one arrives via environment variable:
| Source | Name | Maps to |
|--------|------|---------|
| Argument | `--session-id` | `SessionId` |
| Argument | `--pipe-name` | `PipeName` |
| Argument | `--protocol-version` | `ProtocolVersion` |
| Env var | `MXGATEWAY_WORKER_NONCE` | `Nonce` |
The nonce travels via environment variable rather than an argument because process command lines are visible to other users on Windows through `wmic`, `Get-CimInstance Win32_Process`, and the kernel object table; environment variables of another process are not. Treating the nonce as a credential keeps it off the command line.
There are no optional options. An unknown flag, a flag without a value, or a flag whose value starts with `--` is reported as an error rather than silently ignored.
## The Parser
`WorkerOptionsParser` walks `args` once, collects values into a case-insensitive dictionary, and accumulates errors so the caller sees every problem in a single failure rather than fixing them one at a time.
```csharp
for (int index = 0; index < args.Length; index++)
{
string arg = args[index];
if (!IsKnownOption(arg))
{
errors.Add($"Unknown option '{arg}'.");
continue;
}
if (index + 1 >= args.Length || args[index + 1].StartsWith("--", StringComparison.Ordinal))
{
errors.Add($"Option '{arg}' requires a value.");
continue;
}
values[arg] = args[index + 1];
index++;
}
```
After argument scanning, the parser cross-checks the protocol version against `GatewayContractInfo.WorkerProtocolVersion`. A version that parses as a `uint` but does not match the contract value is a hard failure with `WorkerExitCode.InvalidProtocolVersion`, separate from `InvalidArguments`, so the gateway can distinguish a malformed launch from a version mismatch and report a useful upgrade message.
The nonce is read last so that argument-shape errors are reported before the parser asks the environment for a secret it might not need.
## Bootstrap Result
`WorkerBootstrapResult` is a discriminated success/failure carrier. `Options` is non-null when `Succeeded` is true; `Errors` is populated only on failure.
```csharp
public static WorkerBootstrapResult Success(WorkerOptions options)
{
return new WorkerBootstrapResult(WorkerExitCode.Success, options, []);
}
public static WorkerBootstrapResult Failure(WorkerExitCode exitCode, IEnumerable<string> errors)
{
return new WorkerBootstrapResult(exitCode, null, errors.ToArray());
}
```
`Succeeded` is defined as `ExitCode == WorkerExitCode.Success` rather than as a separate flag, so the exit code and the success state cannot disagree.
## Exit Codes
`WorkerExitCode` is the worker process's exit contract. The gateway-side launcher decodes these values to decide whether a relaunch is safe.
| Value | Numeric | Produced when |
|-------|---------|---------------|
| `Success` | 0 | The pipe session ran to a clean close. |
| `UnexpectedFailure` | 1 | Any unhandled exception not matched by a more specific catch. |
| `InvalidArguments` | 2 | One or more `--session-id`, `--pipe-name`, or `--protocol-version` errors (missing, empty, unknown flag, or no value). |
| `InvalidProtocolVersion` | 3 | `--protocol-version` is not a `uint` or does not equal `GatewayContractInfo.WorkerProtocolVersion`. |
| `MissingNonce` | 4 | The `MXGATEWAY_WORKER_NONCE` environment variable is null, empty, or whitespace. |
| `PipeConnectionFailed` | 5 | An `IOException` or `TimeoutException` escapes the pipe client. |
| `ProtocolViolation` | 6 | A `WorkerFrameProtocolException` escapes the pipe client. |
`InvalidArguments`, `InvalidProtocolVersion`, and `MissingNonce` originate in the parser; the others originate in `WorkerApplication.Run`'s `try/catch` around the pipe client.
## Environment Abstraction
`IWorkerEnvironment` exists so tests can supply a fake nonce without mutating the real process environment, which would be a shared mutable global across parallel test runs.
```csharp
public interface IWorkerEnvironment
{
string? GetEnvironmentVariable(string name);
}
public sealed class EnvironmentVariableWorkerEnvironment : IWorkerEnvironment
{
public string? GetEnvironmentVariable(string name)
{
return Environment.GetEnvironmentVariable(name);
}
}
```
The production binding in `WorkerApplication.Run(string[])` is `EnvironmentVariableWorkerEnvironment`, which is a thin pass-through to `System.Environment.GetEnvironmentVariable`.
## Logging
The worker writes structured key/value lines to standard error. Standard error is used rather than standard output because the gateway side reads worker stdout for diagnostic capture only, while stderr is reserved for log output that does not interfere with any future stdout-based channel.
### The logger contract
`IWorkerLogger` exposes only `Information` and `Error`. There is no `Debug` or `Trace` level, because the worker is launched per session and verbose tracing belongs to the gateway-side launcher.
```csharp
public interface IWorkerLogger
{
void Information(string eventName, IReadOnlyDictionary<string, object?> fields);
void Error(string eventName, IReadOnlyDictionary<string, object?> fields);
}
```
`WorkerConsoleLogger` formats each call as `level=<Level> event=<EventName> key=value key=value` after running the field dictionary through `WorkerLogRedactor`:
```csharp
private void Write(
string level,
string eventName,
IReadOnlyDictionary<string, object?> fields)
{
Dictionary<string, object?> redactedFields = WorkerLogRedactor.RedactFields(fields);
string fieldText = string.Join(
" ",
redactedFields.Select(field => $"{field.Key}={FormatValue(field.Value)}"));
_writer.WriteLine($"level={level} event={eventName} {fieldText}".TrimEnd());
}
```
### What the redactor redacts and why
`gateway.md` "Security" requires that the worker never log raw credential values for `AuthenticateUser`, `WriteSecured`, or related secured operations. The bootstrap nonce is also a credential: anyone who reads it can impersonate the worker to the gateway pipe. `WorkerLogRedactor` enforces this by replacing values whose field name contains any of these substrings (case-insensitive) with the literal `[redacted]`:
```csharp
private static readonly string[] SensitiveFieldNameParts =
[
"nonce",
"secret",
"password",
"token",
"credential",
"apikey",
"api_key",
];
```
The match is on substrings of the field name rather than an exact list, so a field called `auth_token` or `user_password` is redacted automatically without each call site having to remember to opt in. `null` values pass through unchanged so the absence of a value is still visible in logs.
## How `Program.cs` Consumes The Result
`WorkerApplication.Run` is the single consumer of `WorkerBootstrapResult`. On failure it logs a `WorkerBootstrapFailed` event and returns the numeric `ExitCode` directly:
```csharp
WorkerOptionsParser parser = new(environment);
WorkerBootstrapResult result = parser.Parse(args);
if (!result.Succeeded)
{
logger.Error("WorkerBootstrapFailed", new Dictionary<string, object?>
{
["exit_code"] = result.ExitCode,
["errors"] = string.Join(";", result.Errors),
});
return (int)result.ExitCode;
}
```
On success it logs `WorkerBootstrapSucceeded` with the session fields (the `nonce` field is redacted by `WorkerLogRedactor` because of its name), hands the `WorkerOptions` to `IWorkerPipeClient.RunAsync`, and waits synchronously. The `try/catch` around the pipe call maps `WorkerFrameProtocolException` to `ProtocolViolation`, `IOException`/`TimeoutException` to `PipeConnectionFailed`, and any other exception to `UnexpectedFailure`, so every code path through `Run` returns one of the values in `WorkerExitCode`.
## Related Documentation
- [Worker Process Launcher](./WorkerProcessLauncher.md)
- [Worker STA](./WorkerSta.md)
- [Worker Frame Protocol](./WorkerFrameProtocol.md)