mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-28 12:04:41 +01:00
docs(runner): document authenticated phase 3 transport
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
313c40cd9b
commit
816aedb561
1 file changed
+59
-18
@@ -5,9 +5,12 @@ Rust `paperclip-runnerd` process is the client. The TypeScript mock core is the
|
||||
remote peer. Neither side imports Paperclip server, UI, database, or shared
|
||||
control-plane code.
|
||||
|
||||
This is a local reliability proof. The mock uses `ws://127.0.0.1` so tests do
|
||||
not need certificates. A production bridge must use `wss://` and remains a
|
||||
later, separately reviewed phase.
|
||||
This is a local reliability and authenticated-transport proof. The mock keeps
|
||||
the RFC 6455 carrier on `ws://127.0.0.1`, but PRP application frames use a
|
||||
mutually authenticated, encrypted session. Loopback position and the
|
||||
WebSocket acceptance header are not authentication. A production bridge should
|
||||
still use `wss://` for defense in depth and remains a separately reviewed
|
||||
deployment phase.
|
||||
|
||||
## Connection and authentication
|
||||
|
||||
@@ -16,23 +19,36 @@ The connection starts in this order:
|
||||
1. The mock core creates a random bootstrap ticket with a five-second lifetime.
|
||||
2. The ticket is passed to the runner through
|
||||
`PAPERCLIP_RUNNER_BOOTSTRAP_TICKET`. It is not a command-line argument.
|
||||
3. The runner opens an outbound WebSocket and sends the ticket in the HTTP
|
||||
`Authorization` header.
|
||||
4. The runner sends a PRP v1 `hello` envelope. It names the stable runner and
|
||||
environment lease, approved runner version and digest, platform,
|
||||
capabilities, and durable resume cursors.
|
||||
5. The mock core consumes the ticket once and sends a `welcome` envelope. The
|
||||
welcome selects PRP v1, returns a short-lived connection lease, reports the
|
||||
cumulative committed event cursor, and carries at most one pending command.
|
||||
6. Later connections authenticate with the connection lease. A real runner
|
||||
3. The runner opens an unauthenticated WebSocket upgrade with no bearer header,
|
||||
then sends only a public credential locator, a fresh client nonce, complete
|
||||
runner/run/session identity, approved runner version and digest, negotiated
|
||||
protocol range, and durable resume cursors.
|
||||
4. The core returns a fresh server nonce plus an HMAC-SHA-256 proof over the
|
||||
complete transcript. The proof binds runner, environment lease, run,
|
||||
normalized session, turn, item, runner artifact, selected protocol,
|
||||
credential/connection lease identity, expiry, and revocation epoch.
|
||||
5. The runner validates that proof before returning its own transcript-bound
|
||||
proof. The bootstrap ticket or connection lease token itself never crosses
|
||||
the socket. A failed proof does not consume a one-use bootstrap ticket.
|
||||
6. Both sides derive directional AES-256-GCM keys from the capability and both
|
||||
nonces. Strict per-direction counters reject replays and out-of-order frames.
|
||||
7. Only after mutual authentication does the core send an encrypted `welcome`.
|
||||
The welcome selects PRP v1, returns a short-lived connection lease, reports
|
||||
the cumulative committed event cursor, and carries at most one pending
|
||||
command. Every later ACK, command, revoke, event, and command result remains
|
||||
inside the encrypted session.
|
||||
8. Later connections authenticate with the connection lease. A real runner
|
||||
process restart receives a new one-time bootstrap ticket and loads the same
|
||||
durable state before it connects.
|
||||
|
||||
The runner keeps the live connection lease token in memory only. Its state file
|
||||
does not contain the bootstrap ticket or connection lease token. The mock core
|
||||
persists only SHA-256 digests of those capabilities. A used or expired ticket is
|
||||
rejected with `401`. The mock also rejects a changed runner identity, environment
|
||||
lease, runner version, or runner digest.
|
||||
persists domain-separated SHA-256 authentication keys instead of raw
|
||||
capabilities. Used, expired, revoked, or identity-mismatched credentials fail
|
||||
closed during the challenge. The runner validates the complete encrypted
|
||||
welcome and every control envelope against the authenticated connection,
|
||||
runner, environment lease, run, normalized session, turn, item, protocol,
|
||||
lease ID, expiry, and revocation metadata before applying an ACK or command.
|
||||
|
||||
The daemon captures and removes the bootstrap environment variable before it
|
||||
parses arguments or starts child work. Secret buffers are overwritten when they
|
||||
@@ -67,13 +83,19 @@ The state contains:
|
||||
- stable runner, environment lease, run, session, turn, and item IDs;
|
||||
- next source event sequence and cumulative acknowledged source sequence;
|
||||
- unacknowledged event envelopes and their byte counts;
|
||||
- a processed-command cache with a non-secret command digest and prior result;
|
||||
- a bounded recent processed-command cache with a SHA-256 command digest and
|
||||
prior result, plus a fixed-size fail-closed replay filter for compacted IDs;
|
||||
- lifecycle, reconnect, backpressure, harness generation, and recovery facts;
|
||||
- bounded, redacted diagnostics.
|
||||
|
||||
Authentication capabilities and arbitrary command bodies are not stored. A
|
||||
command is represented by an FNV-1a comparison digest, its stable ID and
|
||||
controller sequence, the redacted result, and the logical-effect count.
|
||||
recent command is represented by a SHA-256 comparison digest from the vetted
|
||||
RustCrypto implementation, its stable ID and controller sequence, the redacted
|
||||
result, and the logical-effect count. The exact cache keeps at most 128 entries.
|
||||
Older IDs are added to a fixed 4 KiB Bloom filter before their exact records are
|
||||
removed. A possible filter match is rejected with zero effects, so Bloom false
|
||||
positives can reject new work but can never make an old command effective again.
|
||||
The durable controller sequence remains monotonic across compaction and restart.
|
||||
|
||||
## Event delivery and ACKs
|
||||
|
||||
@@ -113,6 +135,25 @@ The trace records `logicalEffectCount`. Every completed command has exactly one
|
||||
logical effect. A policy rejection, such as a new turn during drain or storage
|
||||
pressure, has zero effects.
|
||||
|
||||
## Residual local trust and revocation window
|
||||
|
||||
The authenticated session removes trust in whichever process wins the configured
|
||||
loopback port: a relay can forward opaque bytes, but it cannot learn a bootstrap
|
||||
or lease capability, decrypt control data, or forge a welcome, ACK, command, or
|
||||
revoke frame. The remaining local-host assumption is that the OS protects the
|
||||
runner process memory, inherited bootstrap environment at launch, private
|
||||
`0700`/`0600` state paths, and the mock core's derived authentication keys from
|
||||
other same-user processes with debugging, memory-reading, or filesystem access.
|
||||
An attacker with those privileges is outside this transport boundary.
|
||||
|
||||
Lease expiry is checked locally before control data is applied. Explicit
|
||||
revocation reaches an already-connected runner through an authenticated revoke
|
||||
frame; if that frame cannot be delivered, the session remains usable until the
|
||||
connection closes or the runner reaches the signed lease expiry. The mock uses
|
||||
a 30-second lease, so that is the maximum demonstrated revocation window. A
|
||||
production core should close active sessions when it revokes a lease and choose
|
||||
the lease TTL to match its required revocation bound.
|
||||
|
||||
## Restart and reconciliation
|
||||
|
||||
A socket drop keeps the same Rust process and in-memory lease. The process
|
||||
|
||||
Reference in new issue
Block a user