Canonical design:
four-file-cloud/docs/interprocess-go-concept.md. Vectors live intestdata/and are asserted from Go and from the Rust interop harness.
These are the artefacts that freeze the contract. They live in testdata/ and are asserted by
tests on every platform; the Rust interop suite asserts the same bytes from the other side.
maxSize = 16777216 (16 MiB) for every vector. Payload bytes are shown in hex.
| Case | Payload | Encoded frame | Expectation |
|---|---|---|---|
| empty | (none) | 00 00 00 00 |
round-trips to a zero-length, non-nil slice |
| single byte | 41 |
00 00 00 01 41 |
round-trips |
| multi-byte UTF-8 | c3 9f (ß) |
00 00 00 02 c3 9f |
the library never inspects payload encoding |
| JSON envelope | 7b 7d ({}) |
00 00 00 02 7b 7d |
framing is payload-agnostic |
| 1 MiB pattern | i mod 251 for i in [0,1048576) |
00 10 00 00 + payload |
exercises the large-write path |
| at the limit | 16777216 bytes | 01 00 00 00 + payload |
accepted |
| over the limit | declared 01 00 00 01 |
— | ErrFrameTooLarge before allocation |
Big-endian is normative. The over-the-limit case must be rejected from the prefix alone; a reader that allocates first is a remote memory-exhaustion primitive, and this vector is what proves it does not.
| Case | Input | Expectation |
|---|---|---|
| fragmented prefix | 4-byte prefix delivered one byte per Read |
assembles correctly |
| fragmented payload | payload delivered in 1-byte chunks | assembles correctly |
| clean EOF | stream ends exactly at a frame boundary | io.EOF |
| truncated payload | prefix declares 8, only 3 bytes follow, then EOF | ErrShortFrame (wrapping io.ErrUnexpectedEOF) |
two frames, one Read |
both frames in a single buffer | two successive ReadFrame calls succeed |
io.EOF at a frame boundary and a truncated frame must be distinguishable. Conflating them makes
a killed peer look like a clean shutdown.
| Input | Result |
|---|---|
file-core-agent |
valid |
file-core-agent-01 , a.b_c-d |
valid |
| `` (empty) | ErrInvalidName |
has/slash , has\backslash |
ErrInvalidName |
has space , ümlaut |
ErrInvalidName — ASCII [A-Za-z0-9._-] only |
| 200-char identifier | resolves via the Decision 6 rule; the vector pins the exact output string |
Resolution shapes, asserted per platform:
Linux <runtime_dir>/interprocess-go/<id>.sock dir 0700, socket 0600
macOS <runtime_dir>/interprocess-go/<id>.sock dir 0700, socket 0600
Windows \\.\pipe\interprocess-go\<user-sid>\<id> DACL: current user SID only
Go listener ↔ Rust client and Rust listener ↔ Go client, on every platform in the support matrix, exchanging every V1 vector and asserting byte equality in both directions. A vector that only one language can produce is not a contract.
- name validation and deterministic resolution
- length, character set, and collision handling
- options and secure defaults
- framing with empty, small, large, fragmented, and invalid frames
- echo between listener and client
- multiple parallel clients
- listener close terminates
Accept - context cancel during dial
- restart after a clean close
- restart after a simulated crash, i.e. a stale Unix socket
- permission/DACL tests, run on a real machine of that platform
- Rust
interprocess↔ Gointerprocess-go - byte-exact test vector for the reference frames
- versioned test scenarios in the repository, not just manual demos
There is no test CI. .github/workflows/ carries deploy and release workflows only; runner
minutes are not spent re-running a gate that already passed on the author's machine.
Linux required before merging any change to Unix code
macOS required before merging any change to Unix code
Windows required before merging any change to named-pipe code
Go the supported Go versions
Rust stable toolchain, for the interop harness
A change confined to documentation or to one platform's files needs only that platform. A change to shared code — name validation, framing, options — needs all of them. "Someone else will run it" is how a platform bug ships.
Each phase is one issue, one branch, one PR, and is independently verifiable. A phase is done when every criterion below passes — a criterion that cannot be asserted by a test is not a criterion.
Scope: Linux and macOS. No Windows code, no framing.
| # | Acceptance criterion |
|---|---|
| 1.1 | Listen and Dial work for Filesystem, Namespaced and UserScoped on Linux and macOS. |
| 1.2 | Name validation matches vector V3 exactly, including the 200-character case and the Decision 6 truncation output. |
| 1.3 | Runtime directory resolution follows Decision 2 in order, and every candidate is rejected unless it exists, is a directory, is owned by the current UID, and has no group or world write bit. A test sets $TMPDIR to a 0777 directory on Linux and asserts it is skipped. |
| 1.4 | With no valid candidate, Listen fails with ErrNoRuntimeDir and creates nothing. |
| 1.5 | After Listen, the runtime directory is mode 0700 and the socket file is mode 0600, asserted by os.Stat. |
| 1.6 | Stale-socket reclaim follows the documented six steps. A test writes a regular file at the socket path and asserts ErrStaleCleanupUnsafe — only an owned socket is ever removed. |
| 1.7 | Close() removes the listener's own socket file; a second Listen on the same name then succeeds. |
| 1.8 | Close() causes a blocked Accept() to return, and the returned error satisfies errors.Is(err, net.ErrClosed). |
| 1.9 | Dial honours context cancellation and DialOptions.Timeout; a cancelled dial returns promptly and leaks no goroutine (goleak or an equivalent check). |
| 1.10 | PeerIdentity() returns the correct UID and GID (SO_PEERCRED on Linux, getpeereid on macOS). |
| 1.11 | Echo across 64 concurrent clients, 1 MiB per client, with no data corruption and no race under -race. |
| 1.12 | Restart after a simulated crash (process killed, socket file left behind) succeeds. |
| 1.13 | examples/echo builds and runs on both platforms. |
| 1.14 | Suite verified locally on Linux and macOS with -race enabled. |
| # | Acceptance criterion |
|---|---|
| 2.1 | Listen and Dial work over named pipes via go-winio, returning net.Listener and net.Conn. |
| 2.2 | Names resolve to \\.\pipe\interprocess-go\<user-sid>\<id> exactly as vector V3 states. |
| 2.3 | The pipe DACL grants the current user SID only. A test running as the same user connects; the DACL is read back and asserted to contain no Everyone, Anonymous, or Authenticated Users entry. |
| 2.4 | AccessCurrentLogonSession additionally restricts to the logon SID, and is not the default. |
| 2.5 | Filesystem(path) returns ErrUnsupportedName on Windows — explicitly, never a silent fallback. |
| 2.6 | PeerIdentity() returns the client SID (GetNamedPipeClientProcessId, then the process token). |
| 2.7 | Every Phase 1 behavioural test that is not Unix-specific passes unchanged on Windows: echo, 64 concurrent clients, Close unblocks Accept, dial cancellation, restart. |
| 2.8 | Suite verified locally on Windows. |
| # | Acceptance criterion |
|---|---|
| 3.1 | A Rust harness using interprocess 2.4.x connects to a Go listener, and a Go client connects to a Rust listener, on all three platforms. |
| 3.2 | Both directions exchange every V1 vector and assert byte equality — vector V4. |
| 3.3 | The Rust side addresses the endpoint with GenericFilePath against the literal path on Unix; the harness contains a comment stating why GenericNamespaced would fail. |
| 3.4 | The Tier table in this document is re-checked against the then-current interprocess release and updated in the same PR. |
| 3.5 | README carries the security model, the platform differences, a minimal example, and the compatibility matrix. |
| # | Acceptance criterion |
|---|---|
| 4.1 | WriteFrame and ReadFrame produce and accept every V1 vector byte-exactly. |
| 4.2 | Every V2 reader case passes: fragmented prefix, fragmented payload, clean io.EOF at a boundary, ErrShortFrame on truncation, two frames in one buffer. |
| 4.3 | A declared length above maxSize returns ErrFrameTooLarge without allocating, proven by a test whose declared length would exhaust memory if allocated first. |
| 4.4 | io.EOF and ErrShortFrame are distinguishable by errors.Is. |
| 4.5 | A fuzz target over ReadFrame runs locally with no crashers, and its corpus is committed. |
| 4.6 | examples/request_reply demonstrates a versioned JSON envelope over the framing. |