eggserve-core 0.1.2

Security policy, path confinement, and static-serving primitives for eggserve
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
# Release Contract

This document defines the exact product surface, behavioral guarantees, and compatibility commitments for eggserve's first public release. It is the normative reference for what eggserve ships, what is stable, what is experimental, and what is internal.

Version: 0.1.0 (pre-release)

Request-body behavior is service-owned. The built-in static service rejects
body-bearing requests, while custom services may declare buffering or streaming
for the actual method. The runtime owns one server-wide file-stream admission
pool and closes incomplete streamed requests.

The Python wheel compatibility declaration is CPython 3.11+ with abi3 stable ABI
(`>=3.11`) on Linux, macOS, and Windows. The wheel contains the matching
platform-native extension and its extension-backed CLI entry point; PyPy and
free-threaded CPython are not supported.

## Release Artifacts

| Artifact | Description | Distribution |
|----------|-------------|--------------|
| `eggserve` binary | CLI static file server | `cargo install` |
| `eggserve-core` crate | Rust library for path confinement, policy, response planning | crates.io (planned) |
| Python wheel `eggserve` | Python package containing the native extension, CLI entry point, and Rust primitives | PyPI (planned) |

### Feature Gates

| Feature | Default | Enables |
|---------|---------|---------|
| (none) | Yes | Core server + primitives |
| `python-bindings-internal` | No | `ResolvedFile` extraction methods for Python bindings only |

## Runtime Service Boundary (Experimental)

**Stability**: All `server` module types are **experimental**. The interface may change in any release.

The `server` module provides a reusable, transport-owning HTTP runtime for embedding. It owns the TCP accept loop, connection management, optional TLS (feature-gated), and HTTP/1 connection handling. Downstream projects implement the `Service` trait and provide it to `Server`; the runtime handles transport concerns.

### Exposed Types

| Type | Description |
|------|-------------|
| `Server` | Main entry point; creates via `Server::builder()` |
| `ServerBuilder` | Configured builder for `Server`; supports `.bind()` and `.from_listener()` for existing listeners |
| `ServerHandle` | Control handle: `local_addr()`, `shutdown()`, `wait()`, `ready()`, `force_shutdown()`, `state()` |
| `RuntimeConfig` | Transport-level configuration (bind, limits, timeouts, body ceiling, optional TLS) |
| `Service` trait | Receives `Request` (envelope: `RequestHead` + `RequestBody` + `ConnectionInfo`), returns `Result<Response, ServiceError>` |
| `service_fn` | Create a `Service` from a closure |
| `StaticService` | Hardened static file service implementing `Service` |
| `ServiceError` | Per-request errors: Internal, Rejected, Panic, Timeout |
| `ServerError` | Startup/lifecycle errors: Bind, Config, AlreadyStarted, Accept, ShutdownTimeout, Startup, Terminal |
| `LifecycleState` | Lifecycle state machine: Created → Starting → Running → Draining → Stopped/Failed |
| `ShutdownResult` | Returned by shutdown operations, carries final `LifecycleState` |

### Guarantees

- Canonical `Request` envelope (containing `RequestHead`, `RequestBody`, `ConnectionInfo`) is passed to services
- Canonical `Response` is returned by services; the runtime normalizes and sends it
- Hop-by-hop header stripping and content-length computation are runtime-owned
- Handler panics are caught at the tokio task boundary and map to `ServiceError::Panic`
- Handler timeouts map to `ServiceError::Timeout`
- Filesystem policy (symlinks, dotfiles, listing) belongs to the service, not the runtime
- `StaticService` provides all the security properties of the built-in static handler
- Lifecycle state transitions are race-safe: `shutdown()` and `force_shutdown()` are idempotent
- No permit leakage: all connection semaphore permits are released on connection drop or shutdown
- `ready()` resolves once the server is accepting connections (no false positives)

### Lifecycle guarantees

- Lifecycle state transitions are race-safe (atomic CAS)
- State transitions: `Created → Starting → Running → Draining → Stopped`, with `Created → Failed` and `Starting → Failed` on errors
- Double-start returns `ServerError::AlreadyStarted`
- Shutdown before start is a no-op
- Multiple shutdown calls are idempotent
- Dropping `ServerHandle` triggers graceful shutdown
- `ready().await` returns immediately if already running
- `ready().await` returns error if server failed during startup
- `force_shutdown(deadline)` returns `ShutdownResult::Forced` on deadline exceeded

### Request Body Primitives (Experimental)

**Stability**: All request body types are **experimental**. The interface may change in any release.

**Framing strictness**: Requests containing both Transfer-Encoding and Content-Length are rejected with 400 before service invocation. When Hyper's HTTP parser normalizes headers (stripping Content-Length when Transfer-Encoding is present), the rejection occurs if both headers survive parser extraction. Duplicate Content-Length values are always rejected at the HTTP/1 wire level. This behavior applies uniformly to both built-in static service and custom services.

| Type | Description |
|------|-------------|
| `RequestBodyPolicy` | Body acceptance policy: `Reject`, `Buffer { max_bytes }`, `Stream { max_bytes }` |
| `RequestBody` | One-shot, bounded request body with `read_all` and streaming |
| `BodyState` | Body consumption state machine: Unread, Streaming, Complete, Error |
| `RequestBodyError` | Typed body error taxonomy (policy, limit, timeout, disconnect, consumption state) |
| `Request` | Canonical request envelope: `RequestHead` + `RequestBody` + `ConnectionInfo` |
| `Service::call(Request)` | Service trait now accepts `Request` instead of `RequestHead` |
| `RuntimeConfig::max_request_body_bytes` | Hard body size ceiling (default 0) |
| `Service::request_body_policy(&RequestHead)` | Service-declared body policy, bounded by the runtime ceiling |
| `IncompleteBodyPolicy` | Public marker for the close-on-incomplete-body behavior; no runtime configuration field |

Body acceptance plumbing (Hyper Incoming → RequestBody) is implemented in the
connection pipeline. The runtime default remains body rejection, while custom
services may opt into bounded buffering or streaming under the configured hard
ceiling.

### Python Body Parity (Milestone 4C)

**Stability**: All Python request body types are **experimental**. The interface may change in any release.

The Python body surface projects the Rust `RequestBody` contract through PyO3:

| Python Type | Description |
|-------------|-------------|
| `RequestBody` | One-shot, bounded request body; `read()` returns `bytes`, `iter_chunks()` yields `bytes` chunks |
| `BodyChunkIterator` | Synchronous iterator over body chunks |
| `RequestBodyError` | Base body error (child of `EggserveError`) |
| `RequestBodyRejectedError` | Body rejected by policy |
| `RequestBodyTooLargeError` | Body exceeds byte limit |
| `RequestBodyTimeoutError` | Body read timed out |
| `RequestBodyDisconnectedError` | Client disconnected during body read |
| `RequestBodyIncompleteError` | Body incomplete |
| `RequestBodyConsumedError` | Body already consumed (one-shot enforcement) |
| `RequestBodyCancelledError` | Body consumption cancelled |

Key properties:
- Default body policy is `reject` (backward-compatible with Milestone 3).
- `read()` and `iter_chunks()` are mutually exclusive; mixing raises `RequestBodyConsumedError`.
- All byte limits and timeouts are enforced by the Rust runtime, not Python.
- Python methods release the GIL during Rust-owned I/O waits.
- Static service remains bodyless (`RequestBodyPolicy::Reject`).
- Callback timeout does not cancel arbitrary Python code; timed-out callbacks still count against concurrency until they return.

## Structured Logging

**Stability**: The `ops` module is **experimental**. Event schema and counter semantics may evolve.

- **Event schema version**: 1. All emitted events include `schema_version: 1`.
- **JSON Lines format**: Each event is one valid JSON object per line on stderr. No array wrapping, no trailing commas.
- **Text sanitization**: Control characters, bidi controls, and escape sequences are stripped from text output. Long fields are truncated.
- **Counter semantics**: `OpsCounters` uses atomic increments with relaxed ordering. Counter values are approximate under concurrent access (no snapshot consistency guarantee).
- **Correlation ID uniqueness**: Connection IDs are unique per-process 64-bit identifiers. Request sequence numbers are monotonically increasing within a connection.

## Supported Protocol

- **HTTP/1.1 only** — no HTTP/2 or HTTP/3.
- **Read-only methods**: GET and HEAD. All other methods return 405.
- **Request target**: origin-form only (`/path?query`). Authority-form and absolute-form are rejected.
- **Static request bodies**: the built-in static service rejects body-bearing
  requests before method dispatch. Custom services may accept bodies through
  their declared policy; TRACE content remains rejected globally.
- **Conditional requests**: `If-None-Match`, `If-Modified-Since`, `If-Range` are supported.
- **Range requests**: `Range` header with single byte ranges. Multi-range returns the full response.
- **HEAD parity**: HEAD responses include the same headers as GET with an empty body.

## Wire-level framing rules

These rules are enforced at the raw HTTP/1.1 socket boundary and are regression-tested in `crates/eggserve-core/tests/http_wire_correctness.rs`.

### Request rejection rules (eggserve policy)

- Non-GET/HEAD methods → 405 with `Allow: GET, HEAD`.
- Absolute-form (`http://host/path`) → 400.
- Authority-form (`host:port`), asterisk-form (`*`) → 405 (method check fires before target-form check).
- Empty or missing path → 400.
- Paths containing NUL bytes, backslashes, or encoded separators (`%2f`, `%5c`) → 400.
- Path traversal beyond root (`/../`) → 400 or 403 (path confinement denial).
- Percent-encoded dot-dot traversal (`%2e%2e`, `%252e%252e`) → 400 or 403.
- Root path (`/`) with directory listing disabled → 403.
- Semicolons in paths are literal characters (not path separators) → 404 if no matching file.
- Positive content on the static service → 413.
- Invalid `Content-Length` (non-numeric, negative, overflow) → 400 or connection close.
- `Transfer-Encoding` on the static service → 400.
- Both `Content-Length` and `Transfer-Encoding` present → 400.
- `Transfer-Encoding` with unsupported codings (e.g. `chunked`) → 400.
- Duplicate `Content-Length` with conflicting values → 400.
- Duplicate `Content-Length` with identical values → treated as single `Content-Length`.
- Comma-joined `Content-Length` values (e.g. `Content-Length: 0, 10`) → 400.
- Request body bytes present without framing headers → connection closed (premature EOF).
- Truncated body mid-stream → connection closed, no response sent.
- Malformed chunk encoding → 400 or connection close.
- Body limit exceeded mid-stream → 413 or connection close.
- Body read timeout → 408 or connection close.

### Parser-level behavior (hyper)

These behaviors are determined by hyper's HTTP/1.1 parser, not eggserve policy:

- HTTP/1.0 requests are accepted (hyper accepts any `HTTP/x.y` version line).
- Bare LF (without CR) in header values is accepted by hyper's parser.
- CR+LF in header values is parsed as a header separator by hyper, resulting in two separate headers.
- Malformed header names or values that hyper cannot parse result in connection closure.
- Requests with invalid byte sequences in header names result in connection closure.

### Response guarantees (wire-tested)

- `Accept-Ranges: bytes` present on all file responses (200, 206, 416).
- `Content-Length` matches actual body bytes for all responses.
- HEAD responses include all headers but suppress body transfer.
- 304 responses include `ETag` and `Last-Modified` (when available), no body.
- 416 responses include `Content-Range: bytes */TOTAL` and `Content-Length: 0`.
- `X-Content-Type-Options: nosniff` present on all file responses.
- 405 responses include `Allow: GET, HEAD`.

## Behavioral Guarantees

### Safe Defaults (enforced at library level)

- Loopback bind (`127.0.0.1`) unless `--public` is passed
- Symlinks denied unless `--follow-symlinks` is passed
- Dotfiles denied unless `--allow-dotfiles` is passed
- Directory listing disabled unless `--directory-listing` is passed
- Unknown MIME types served as `application/octet-stream`
- Malformed request targets rejected

### Path Confinement

- No file is served outside the configured root directory.
- Path traversal, NUL bytes, ambiguous separators, Windows prefixes, reserved names, and ADS syntax are rejected.
- On Unix with safe defaults (symlinks denied): descriptor-relative traversal via `statat(AT_SYMLINK_NOFOLLOW)` + `openat(O_NOFOLLOW)`. A symlink swapped between check and open is refused rather than followed.
- With `--follow-symlinks`: component-wise `symlink_metadata` checks. Weaker than descriptor-relative; explicitly outside the hardening guarantee.
- On Windows: parser-level checks plus handle-relative child resolution and directory enumeration. `ResolvedDirectory` retains an owned handle for child resolution; `RootGuard::resolve_child` uses handle-relative traversal. Directory enumeration uses `NtQueryDirectoryFile` on the retained handle. Windows remains trusted/local-content only because two open-descendant root-rename cases are rejected by NTFS path-rename semantics; see `docs/toolchain-support.md`.

### Resource Limits

| Limit | Default | Enforcement |
|-------|---------|-------------|
| Concurrent connections | 64 | Tokio semaphore |
| Concurrent file streams | 32 | Tokio semaphore |
| Request body size | 0 (rejected) | Header validation |
| Header read timeout | 10s | `tokio::time::timeout` |
| Connection total timeout | 60s | `tokio::time::timeout` |
| Graceful shutdown | 10s | Drain after SIGTERM |
| Stream chunk size | 8 KiB | `Limits::stream_chunk_size` (64 B–1 MiB) |
| Directory listing entries | 4096 | `Limits::max_listing_entries` |
| Directory listing response | 1 MiB | `Limits::max_listing_response_bytes` |

### Callback Server (Python `Server` with handler)

- Handler receives a `Request` object and must return a `Response` object.
- Coroutine handlers (functions returning a coroutine object) are rejected with a 500 response.
- Invalid return types produce a generic 500 Internal Server Error.
- Invalid status codes (outside 100–599, non-three-digit) produce 500.
- Invalid header names (empty) or values (containing NUL, CR, LF) produce 500.
- Handler exceptions produce 500 without leaking tracebacks.
- Handler timeout (`handler_timeout_secs`, default 30s): Python callbacks use the actual Rust runtime's handler timeout mechanism. The timeout is enforced at the transport level by the Rust server, not by a Python-side timer.
- Callback concurrency is bounded (default: 8 concurrent handler calls).
- File-backed responses retain their Rust-owned file capability and stream without copying the file through Python memory.
- HEAD requests still invoke the handler, but the runtime suppresses the body before acquiring file-stream resources.
- Graceful shutdown deadline (`graceful_shutdown_timeout_secs`, default 10s) controls drain time.

## Canonical HTTP Request Types

**Stability**: All canonical request types are **stable** and covered by the
conformance corpus.

The canonical request types provide transport-independent, Hyper-independent value types for inspecting HTTP requests. They are defined in `eggserve_core::primitives` and projected to Python through `eggserve._native`.

### Rust Types

| Type | Module | Description |
|------|--------|-------------|
| `Method` | `primitives::method` | Validated HTTP method (standard + extension). Case-sensitive. |
| `HttpVersion` | `primitives::version` | HTTP/1.0 or HTTP/1.1. |
| `HeaderBlock` | `primitives::header_block` | Ordered, duplicate-preserving header collection. |
| `HeaderName` | `primitives::header_block` | Validated header name (RFC 9110 token). |
| `HeaderValue` | `primitives::header_block` | Validated header value (no CR/LF/NUL). |
| `RequestTarget` | `primitives::request_target` | Validated origin-form target (path + query). |
| `RequestHead` | `primitives::request_head` | Canonical request head: method, target, version, headers. |
| `ConnectionInfo` | `primitives::connection_info` | Transport metadata: local/remote addrs, scheme, TLS. |

**Conversion**: `RequestHead::try_from_hyper()` converts a `hyper::Request<B>` into a canonical `RequestHead`. The conversion is fallible and typed — malformed or unsupported input is rejected before handlers. The resulting `RequestHead` contains no Hyper types.

**Legacy**: `ReadOnlyMethod` (GET/HEAD only) remains stable for existing consumers. `Method` is the canonical type for new code.

### Python Types

| Type | Module | Description |
|------|--------|-------------|
| `Method` | `eggserve._native` | Canonical HTTP method. Frozen. |
| `HttpVersion` | `eggserve._native` | Canonical HTTP version. Frozen. |
| `HeaderBlock` | `eggserve._native` | Duplicate-preserving headers. Frozen. |
| `ConnectionInfo` | `eggserve._native` | Transport metadata. Frozen. |
| `CanonicalRequest` | `eggserve._native` | Canonical request head. Frozen. |

**Functions**: `parse_method(value)`, `parse_http_version(value)` — standalone constructors with typed errors.

**Exceptions**: `MethodError`, `HttpVersionError`, `HeaderError`, `DuplicateHeaderError` — child classes of `EggserveError`.

## Canonical Response Types

**Stability**: All canonical response types are **stable** and covered by the
conformance corpus.

The canonical response types provide transport-independent, Hyper-independent value types for constructing HTTP responses. They are defined in `eggserve_core::primitives::canonical` and enforce response normalization rules at construction and before transport conversion.

### Rust Types

| Type | Module | Description |
|------|--------|-------------|
| `StatusCode` | `primitives::canonical` | Validated HTTP status code (100–599, three-digit only). |
| `ResponseHead` | `primitives::canonical` | Status + validated `HeaderBlock`. |
| `ResponseBody` | `primitives::canonical` | Body representation: `Empty`, `Bytes`, `File`, and `EmptyWithLength`. |
| `Response` | `primitives::canonical` | Complete response: head + body. One-shot consumption. |
| `ResponseBuilder` | `primitives::canonical` | Validated builder for `Response`. |
| `NormalizeRequest` | `primitives::canonical` | Context for response normalization. |
| `ResponseConstructionError` | `primitives::canonical` | Error taxonomy for response construction. |

**Normalization functions**:

- `normalize_response(response, request)` applies the following rules before transport conversion:
  1. HEAD suppression — body discarded, representation headers preserved.
  2. Body-forbidden statuses — 1xx, 204, 205, and 304 bodies discarded.
  3. Hop-by-hop header stripping — `Transfer-Encoding` removed.
  4. Content-Length computation — set to actual body length.
  5. Duplicate end-to-end headers preserved.

- `normalize_metadata(status, headers, body_len, is_head)` is the shared normalization entry point for both in-memory and file-backed response producers. It applies the same framing rules (Transfer-Encoding stripping, Content-Length computation) without consuming a `Response` value. File-streaming producers call this directly.

**Conversion**: `to_hyper_response(response)` converts a normalized canonical `Response` into a Hyper `Response<BoxBody>`. This is the final step before sending on the wire.

### Unified Response Architecture

All response producers converge on `normalize_metadata()` for metadata normalization. This function is the shared normalization entry point for both in-memory and file-backed response producers. It applies:

1. Strip runtime-owned `Transfer-Encoding` — always removed regardless of status.
2. HEAD responses: suppress `Content-Length` only for an empty
   representation; retain the equivalent GET length for non-empty bodies.
3. Body-forbidden statuses (1xx, 204, 205, 304): suppress `Content-Length`.
4. Normal payloads: set `Content-Length` to actual body length.
5. Preserve all other headers (including duplicates).

File and byte responses share the same framing policy: `Transfer-Encoding` is always stripped, and `Content-Length` is computed from actual body length. Handler-provided `Content-Length` is overwritten with the computed value.

`normalize_metadata()` is called by `normalize_response()` (for complete `Response` values) and directly by file-streaming producers (for file-backed responses that bypass the canonical `Response` type).

### Response Normalization Algorithm

The normalization algorithm is the single final path for all response producers. It is documented as normative behavior:

**Inputs**: request method/version, response status, response headers, response body metadata, connection policy.

**Rules** (applied in order):
1. HEAD transmits no body bytes while preserving representation headers.
2. 1xx, 204, and 304 transmit no payload body.
3. `Transfer-Encoding` is runtime-owned; handler-supplied values are removed.
4. `Content-Length` is computed by the runtime.
5. Duplicate end-to-end headers are preserved.
6. Error responses do not leak handler tracebacks or internals.

**Fail-versus-strip policy**: The normalization function strips runtime-owned headers (`Transfer-Encoding`) rather than rejecting, because these headers are commonly set by frameworks and rejection would break compatibility. Handler-provided `Content-Length` is overwritten with the computed value.

### Header Representation

#### Response Headers (`HeaderMapPlan`)

- `Vec<ResponseHeader>` — ordered list of `(name, value)` pairs.
- Duplicates are preserved. `get()` returns the first match (case-insensitive).
- No code path currently generates duplicate headers internally.

#### Canonical Request Headers (`HeaderBlock`)

- `Vec<HeaderField>` — ordered list of `(HeaderName, HeaderValue)` pairs.
- Duplicates are preserved. Original field-name casing is preserved.
- Case-insensitive lookup by field name.
- `get_first(name)` returns the first value. `get_all(name)` returns all values. `get_unique(name)` returns an error if duplicates exist.
- Rejects empty names, names exceeding 256 bytes, and values containing CR, LF, or NUL bytes.

#### Python Server (`Response.headers`)

- `HashMap<String, String>` — keys are unique, case-sensitive.
- Duplicate header names (e.g. multiple `Set-Cookie`) are **not representable** — only the last value for a given key survives.
- Request headers are also `HashMap<String, String>` — same limitation on the inbound side.

#### Implication

Python handlers cannot emit duplicate response headers. If a handler needs multiple `Set-Cookie` headers, the handler must combine them into a single value or use the static-responder path which preserves duplicates through `HeaderMapPlan`.

## Conformance Corpus

The conformance corpus defines canonical HTTP type behavior. It contains:

- **Request type conformance**: Method, HttpVersion, HeaderBlock, RequestTarget, RequestHead, and ConnectionInfo parsing and validation rules.
- **Response type conformance**: StatusCode, ResponseHead, ResponseBody, Response construction, and normalization rules.
- **Rust/Python parity tests**: Tests exercising identical behavior across Rust and Python bindings.
- **Normalization conformance**: normalize_response() rules (HEAD suppression, body-forbidden enforcement, hop-by-hop stripping, content-length computation).

The corpus is run by:
- `tests/canonical_conformance.rs` (Rust side)
- `crates/eggserve-python/tests/test_canonical_conformance.py` (Python side)

## API Stability Tiers

Every exported Rust and Python item is classified into one of three tiers:

### Stable

Breaking changes bump the major version. Pre-1.0, minor versions may break stable APIs only with explicit release notes and migration guidance. Patch releases must not break stable APIs. Stable names and signatures are intentionally supported. Semantic behavior identified in this release contract is covered by conformance tests. Unspecified formatting, debug output, log text, and internal implementation details are not stable unless explicitly documented.

### Experimental

May change in any non-patch release. Consumers should pin versions. Functionality is tested but the interface is not frozen. Experimental APIs may be omitted from language parity.

### Internal

Not part of the public contract. Used only for cross-crate communication (e.g. Python bindings). Internal Python names are not exported through `__all__`. Internal Rust features do not become accidental default features. May be removed without notice.

### Compatibility Rules

- **Enum variants** — Stable enum variants are exhaustive unless documented otherwise. Adding a new variant to a stable enum is a breaking change.
- **Exception classes** — Python exception classes and field names are stable. Message strings are not stable.
- **Header ordering** — Rust `HeaderMapPlan` preserves order and duplicates (stable). Python `Response.headers` uses `HashMap` and does not preserve duplicates (known limitation).
- **Error taxonomy**`PathRejection`, `RequestValidationError`, and `ResourceDeniedReason` variants are stable.
- **Serialization**`Debug`, `Display`, and Python `repr()` output are not stable unless documented.
- **Deprecation** — Deprecated stable items remain functional for at least one minor release after announcement.

## Platforms

| Platform | Status | CI | Security Level |
|----------|--------|-----|---------------|
| Linux x86_64 | Supported | Routine CI | Full (descriptor-relative) |
| Linux aarch64 | Supported | Manual | Full (descriptor-relative) |
| macOS arm64 | Supported | Manual | Full (descriptor-relative) |
| macOS x86_64 | Supported | Manual | Full (descriptor-relative) |
| Windows x86_64 | Supported | Manual | Partial (handle-relative child resolution + directory enumeration are qualified; two open-descendant root-rename cases are skipped by NTFS path-rename semantics) |

## Deployment Status

eggserve defines production readiness through explicit profiles rather than one undifferentiated claim. Every production claim must name a profile. Production profiles are maintained by the maintainers and documented in README.md and `docs/deployment.md`.

| Profile | Status | Description |
|---------|--------|-------------|
| unix-reverse-proxy | functional; qualification pending | Linux/macOS behind Caddy/nginx/Traefik (preferred public deployment; external qualification pending) |
| unix-direct-https | functional; qualification pending | Linux/macOS with native rustls (limited HTTP/1.1, not an edge platform; external qualification pending) |
| windows-reverse-proxy | functional | Windows behind reverse proxy; trusted/local content only because two open-descendant root-rename cases are rejected by NTFS path-rename semantics |
| windows-direct-https | functional | Windows with native rustls (parser-level security only) |
| local-development | supported-hardened | Any platform, loopback, safe defaults |
| windows-functional | functional | Windows SMB/non-NTFS/cloud filesystems |
| link-following-compat | functional | Any platform with --follow-symlinks (weaker guarantee) |

Production profile status is maintained by the project maintainers and documented in README.md. No profile has achieved hardened status in this release contract version. This release contract only defines hardened status criteria.

- Reverse-proxy origin (Caddy, nginx, Traefik) is the preferred public deployment.
- Native TLS is limited and does not imply ACME, virtual hosting, HTTP/2, or edge parity.
- Windows hardening is an active roadmap item, not a permanent non-goal.
- Public plaintext HTTP without TLS termination is an unsupported production configuration.

## What This Document Does NOT Cover

- Framework abstractions, routing, middleware, ASGI/WSGI adapters — these are in-tree non-goals. Downstream projects may build them on eggserve primitives, but they are not release deliverables.
- Compatibility with `python -m http.server` beyond practical equivalence — see [compatibility.md]compatibility.md.
- Version freeze — this is a pre-release contract. The API surface may change before 1.0.