Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions CURRENT_STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@

## Active milestone

**S4Analysis parity+ (0.3.0): complete.** S0–S4 are done. S4 reached parity
with the toolkit's analysis surface — report generation (#41), anonymize (#42),
semantic diff (#43), replay (#44), and a headless CLI in the same binary (#45).
Next up is **S5 — Live capture** (the flagship; see [ROADMAP.md](ROADMAP.md)).
**S5Live capture (0.4.0): in progress.** S0–S4 are done — analysis parity, a
headless CLI, and the native inspector. S5 is the flagship: a live WebSocket
proxy between a charge point and its CSMS. First landing is the hardened
RFC 6455 transport codec (#54, ADR-0008); see [ROADMAP.md](ROADMAP.md).

## What's done

Expand Down Expand Up @@ -176,6 +176,6 @@ live timeline with OS notifications on critical failures.
| `docs` (docs, ADRs) | ✅ done for S0 |
| `ocpp` (engine) | ✅ S2 + ingestion (#29) + reports (#41) + anonymize (#42) + diff (#43) + replay core (#44); O(n) detection pending (#36) |
| `ui` (native views) | ✅ S3 inspector (#27–#32) + replay transport (#44) |
| `capture` (live proxy) | ⬜ not started (S5) |
| `capture` (live proxy) | 🔨 S5 in progress: RFC 6455 WS transport codec (#54) |
| `cli` (headless) | ✅ inspect/report/diff/anonymize/ci/scenario (#45) |
| `conformance` | ✅ done for S2 (15/15, `contract-v1`) |
67 changes: 67 additions & 0 deletions docs/adr/0008-websocket-transport.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# ADR-0008 — WebSocket transport: a hand-rolled RFC 6455 subset

- **Status:** Accepted
- **Date:** 2026-07-12

## Context

S5 (live capture) makes Studio a WebSocket man-in-the-middle: a **server** to
the downstream charge point and a **client** to the upstream CSMS, decoding
OCPP-J frames in flight. Zig's standard library has no WebSocket implementation,
so we need one. Two options:

1. **Vendor a dependency** (e.g. a community `websocket.zig`).
2. **Hand-roll** a minimal RFC 6455 subset that Studio owns.

Three constraints shape the choice:

- **The build is zero-config (ADR-0004).** Studio has no `build.zig` /
`build.zig.zon`; the Native SDK drives the build. Adding a third-party
dependency would mean ejecting to a custom build — a large, standing
architectural cost for one protocol.
- **This is the rawest untrusted-input surface in Studio.** Bytes arrive from an
arbitrary peer over a socket. Owning the codec means owning the hardening
(masking enforcement, size bounds, malformed-frame rejection) rather than
trusting a dependency's posture.
- **The needed scope is small and well-specified.** A transparent OCPP-J proxy
needs the RFC 6455 core — opening handshake, base framing, fragmentation,
control frames — and nothing exotic. The standard library already provides the
two handshake primitives (`std.crypto.hash.Sha1`, `std.base64`).

## Decision

**Hand-roll a minimal, hardened RFC 6455 subset in `src/capture/ws.zig`.**

- Keep it a **pure codec**: every function operates on byte buffers, no sockets.
The whole module is unit-testable without a network. Socket I/O and the proxy
loop live in `proxy.zig` (#56).
- Implement **both handshake halves** (server-accept and client-request) and
**both masking rules**, since MITM plays both roles.
- **Enforce hardening in the codec itself:** client→server frames MUST be masked
and server→client MUST NOT (reject violations); bound per-frame and
reassembled-message sizes and fragment counts; reject reserved bits, unknown
opcodes, and malformed control frames.

### In scope

Opening handshake (both halves), base framing (all payload-length forms),
fragmentation reassembly, control frames (close / ping / pong).

### Out of scope

- `permessage-deflate` and other extensions.
- **TLS** — the secure-profile (OCPP 2/3) work is post-0.5 and slots behind this
same codec boundary via a vetted C binding, not `std`.
- Exhaustive protocol edge cases beyond what a transparent OCPP-J proxy needs.

## Consequences

- **No new dependency; the zero-config build (ADR-0004) stays intact.**
- Full control of the untrusted-input hardening at the exact entry point where
it matters most.
- We own RFC 6455 conformance for the subset we implement — mitigated by keeping
the scope bounded and testing the codec exhaustively over byte fixtures.
- TLS and compression, if needed, land behind the same `ws.zig` boundary without
disturbing callers.
- Revisit only if Studio needs extensions (compression) or a breadth of protocol
edge cases that a maintained library would handle better than a bespoke subset.
3 changes: 3 additions & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ supersedes the old one rather than editing history.
| [0003](0003-native-rendered-ui.md) | Native-rendered UI, no WebView | Accepted |
| [0004](0004-zig-native-sdk-zero-config.md) | Zig + Native SDK on the zero-config build | Accepted |
| [0005](0005-engine-value-representation.md) | Engine value representation & version-tagged decoder boundary | Accepted |
| [0006](0006-inspector-builder-view.md) | Inspector as a Zig builder view (not `.native` markup) | Accepted |
| [0007](0007-trusted-ingestion.md) | Trusted-ingestion limits for user-opened traces | Accepted |
| [0008](0008-websocket-transport.md) | WebSocket transport: a hand-rolled RFC 6455 subset | Accepted |
11 changes: 11 additions & 0 deletions src/capture/capture.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//! Aggregate root for the live-capture layer.
//!
//! Like `ocpp/ocpp.zig`, this exists so the root test block pulls the whole
//! subtree into analysis with a single import. Everything here is headless and
//! socket-free at the codec layer; the proxy loop (#56) adds the I/O.

pub const ws = @import("ws.zig");

test {
_ = ws;
}
Loading
Loading