docs: sweep after the fleet fixes; approve the 1.0 spec

- README: custody chain and receipts replace epidemic forwarding; Ack
  frame; running on a robot (host crates, current limits); security;
  crate layout, constants and reading list.
- AGENTS.md: current spec list, receipt constraints and review focus
  7-8, first_seq rule, constants, host crates and the ros2 gate.
- THREAT_MODEL.md: re-checked at 89041ea. F1, F2, F5, F17 fixed; F3
  (single configured peer) and F4 partly fixed; new F18 (no scheduled
  flush, stale clock marker).
- CHANGELOG: Unreleased section for the fleet milestone and fixes.
- tines-bt README: single-peer limit; manual BLE check not yet recorded.
- tines service rustdoc: oversized-frame behaviour, JSON tagging, clock.
- 1.0 spec: approved (2026-10-20), including §10 and the adversarial
  fuzz mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 01:03:08 -07:00
co-authored by Claude Opus 5.5
parent 89041ea89a
commit a4cde40e85
8 changed files with 424 additions and 87 deletions
+32 -5
View File
@@ -18,11 +18,17 @@ architecture in the **spec first**:
- **Core design (authoritative):** `docs/superpowers/specs/2026-10-16-tines-design.md` - **Core design (authoritative):** `docs/superpowers/specs/2026-10-16-tines-design.md`
- **Addressed delivery (authoritative):** `docs/superpowers/specs/2026-10-18-tines-addressed-delivery.md` - **Addressed delivery (authoritative):** `docs/superpowers/specs/2026-10-18-tines-addressed-delivery.md`
- **Fleet deployment v1 (authoritative):** `docs/superpowers/specs/2026-10-18-tines-fleet-deployment-design.md` — supersedes the addressed-delivery spec's epidemic forwarding (custody chain with handoff receipts). - **Fleet deployment v1 (authoritative):** `docs/superpowers/specs/2026-10-18-tines-fleet-deployment-design.md` — supersedes the addressed-delivery spec's epidemic forwarding (custody chain with handoff receipts).
- **Fleet fixes (done):** `docs/superpowers/specs/2026-10-19-tines-fleet-fixes.md` — security-assessment findings F1–F5 and F17.
- **tines-core 1.0 (next, core):** `docs/superpowers/specs/2026-10-18-tines-1-0-design.md` — wire version, public API, signature slot, core hardening (§10).
- **Host hardening (next, hosts):** `docs/superpowers/specs/2026-10-19-tines-host-hardening.md` — findings F10–F12.
- **Implementation plans:** `docs/superpowers/plans/` - **Implementation plans:** `docs/superpowers/plans/`
The plans argue from the specs. Where a plan is silent, the **spec rules**. The plans argue from the specs. Where a plan is silent, the **spec rules**.
Do not edit the specs or the plans — they are design history, not source. Do not edit the specs or the plans — they are design history, not source.
`README.md` is the user-facing overview and should track the code. `README.md` is the user-facing overview and should track the code.
`THREAT_MODEL.md` is the living security assessment: when a change fixes or
changes a finding, update its status there (§1 and §10) with the commit.
`CHANGELOG.md` records each release.
## Architecture (three layers) ## Architecture (three layers)
@@ -68,9 +74,14 @@ call `poll()` in its loop, and `observe()` when it sees something.
- Addressed observations (`to = Some`) never appear in a summary Bloom - Addressed observations (`to = Some`) never appear in a summary Bloom
filter, and an addressed observation past its `lifetime_ms` is never filter, and an addressed observation past its `lifetime_ms` is never
stored or forwarded. stored or forwarded.
- `Node::next_seq()` must be persisted by the application across reboots - `first_seq` passed to `Node::new` must not repeat across reboots
(`boot_count << 32 | counter`); a repeated seq after reboot collides in the (`boot_count << 32`, with a persisted boot counter; `LogStore::first_seq`
ID hash and must dedupe silently (`insert` returns `Ok(false)`). provides it). A repeated seq after reboot collides in the ID hash and must
dedupe silently (`insert` returns `Ok(false)`).
- Receipts: a receive the store rejects (`Err`, e.g. `Full`) is never acked,
addressed or broadcast; an addressed duplicate whose local copy is released
is never acked; an `Ack` is accepted only from a peer the id was sent to
this contact.
- `NodeId = [u8; 16]`, `ObservationId = [u8; 16]`, `Tick = u64` (local - `NodeId = [u8; 16]`, `ObservationId = [u8; 16]`, `Tick = u64` (local
monotonic ms, supplied on every call). monotonic ms, supplied on every call).
@@ -82,12 +93,17 @@ call `poll()` in its loop, and `observe()` when it sees something.
4. Frame from an address with no prior `Contact` → treated as implicit contact. 4. Frame from an address with no prior `Contact` → treated as implicit contact.
5. Tick going backwards → age saturates at stored value, never underflow. 5. Tick going backwards → age saturates at stored value, never underflow.
6. Every emitted frame fits the transport's `mtu()`, including summaries. 6. Every emitted frame fits the transport's `mtu()`, including summaries.
7. Store-full receive → no `Ack`; custody stays with the sender (unit test and
the full-store sim property).
8. Released addressed duplicate → no `Ack`.
### Compile-time constants (`tines-core/src/consts.rs`) ### Compile-time constants (`tines-core/src/consts.rs`)
`MAX_TOPIC=64`, `MAX_PAYLOAD=1024`, `MAX_PEERS=8`, `SUMMARY_BYTES=512`, `MAX_TOPIC=64`, `MAX_PAYLOAD=1024`, `MAX_PEERS=8`, `SUMMARY_BYTES=512`,
`BLOOM_K=3`, `SUMMARY_INTERVAL_MS=2000`, `REASSEMBLY_TIMEOUT_MS=5000`, `BLOOM_K=3`, `SUMMARY_INTERVAL_MS=2000`, `REASSEMBLY_TIMEOUT_MS=5000`,
`SEND_QUEUE_CAP=256`, `MAX_FRAME=1500`. Match the exact values above. `SEND_QUEUE_CAP=256`, `MAX_FRAME=1500`, `ACK_IDS_CAP=90`,
`EVENT_QUEUE_CAP=64`, `BCAST_SENT_CAP=256`, `ACK_BUFFER_CAP=256`. Match the
exact values above.
## Crate layout ## Crate layout
@@ -96,10 +112,21 @@ tines/
tines-core/ no_std core: Observation, Store/Transport/BeliefPolicy traits, tines-core/ no_std core: Observation, Store/Transport/BeliefPolicy traits,
exchange state machine, Node facade, in-memory store, reference exchange state machine, Node facade, in-memory store, reference
policies policies
tines-sim/ std: simulator, contact traces, convergence and relay tines-sim/ std: simulator, contact traces, convergence, relay and custody
property tests property tests
tines-store/ std: LogStore, the persistent append-only backend
tines-bt/ std: BLE transport over BlueZ (bluer)
tines/ std: standalone service, Unix-socket JSON IPC
tines-ros2/ std: ROS2 node; rclrs behind the `ros2` feature
``` ```
Only `tines-core` is headed for 1.0; the host crates stay 0.x (1.0 spec, D4).
They're std-only and may use threads and I/O, but they're still the trust
boundary for radio, IPC and disk input (see `THREAT_MODEL.md`).
`tines-ros2` with `--features ros2` is outside the default gate: check it
with the ROS2 Docker job (`Dockerfile.ros2`, `scripts/check-ros2.sh`).
## Not started ## Not started
`tines-udp`, `tines-sqlite`, `tines-cli`, and `tines-serial` (from the core `tines-udp`, `tines-sqlite`, `tines-cli`, and `tines-serial` (from the core
+79
View File
@@ -4,6 +4,85 @@ All notable changes to the tines crates. Versions follow
[Semantic Versioning](https://semver.org/); before 1.0, a minor bump may break [Semantic Versioning](https://semver.org/); before 1.0, a minor bump may break
the API. the API.
## Unreleased
The fleet-deployment milestone and the fleet fixes (security assessment
findings F1–F5 and F17; see `THREAT_MODEL.md`). The workspace version is
still 0.3.0.
### Added
- **Handoff receipts** (tines-core). The new `Ack` frame (the last `Frame`
variant) carries up to `ACK_IDS_CAP` (90) ids. Acks are sent as the
exchange runs and are accepted only from a peer the id was sent to during
the current contact.
- An ack for an addressed observation moves custody: the sender releases
its copy (`Store::release`) and never offers it again.
- An ack for a broadcast is confidence only. It surfaces as a
`ConfidenceEvent { peer, id }` from the new `Node::drain_events`.
- New constants: `ACK_IDS_CAP`, `EVENT_QUEUE_CAP`, `BCAST_SENT_CAP`,
`ACK_BUFFER_CAP`.
- `wire::frame_sender(bytes) -> Option<NodeId>`: a frame's claimed sender,
for transports that bind a connection to an identity.
- `Node::store_mut`, so a host can call backend methods such as
`LogStore::flush`.
- **New host crates:**
- `tines-store`: `LogStore`, a persistent append-only backend.
- `tines-bt`: a BLE transport over BlueZ.
- `tines`: a standalone service with Unix-socket JSON IPC.
- `tines-ros2`: a ROS2 node; rclrs is behind the `ros2` feature.
### Changed
- **Addressed delivery is a custody chain.** It used to be epidemic
forwarding. A message has one custodian at a time, which hands it on and
releases it when the next holder acks. A lost ack can leave two
custodians for a while; a message is never dropped by the protocol.
- Bounce records carry a direction, so only records of an actual send
validate an ack.
- **`LogStore` log format v1** (incompatible; there's no migration, so delete
old logs):
- The file starts with a `"TINESLOG"` header and a format version.
- Each record has a sync marker, a length and a CRC-32C.
- Ids are recomputed on load, not stored.
- `LogStore` persists a boot counter, and `first_seq()` returns
`boot_count << 32`. Both hosts seed `Node::new` from it, and the
`first_seq` config field is gone.
- `LogStore::release` writes a `Release` record immediately. `flush` writes
a clock marker, so ages carry across a restart.
- `LogStore::open` takes an exclusive lock and creates the file `0600`.
- tines-bt rewrite:
- L2CAP SeqPacket channels, one frame per packet.
- Every node advertises and scans; the smaller `NodeId` dials and the
larger accepts.
- Connections are bound to the advertised `NodeId`, and frames whose
claimed sender differs are dropped.
- Connections emit `Contact` and `Lost`.
### Fixed
- A receiver whose store returned `Err` (for example `Full`) still acked, so
the sender released custody of a message nobody kept. Now nothing is
acked unless it was stored, addressed or broadcast (F1).
- The standalone service never polled its `Node`, so it never exchanged
anything. It now runs the exchange on a poll loop (F2).
- tines-bt had no listener, lost frame boundaries on a stream socket, and
stamped every frame with the configured peer (F3).
- Sequence numbers repeated after every restart, and released flags were
never persisted (F4).
- One corrupt record in the middle of the log truncated everything after it.
Recovery now skips bad records and truncates only a torn tail, with an
fsync of the file and its directory (F5).
- The service's tick was wall-clock *seconds*. Both hosts now use one
monotonic millisecond clock (F17).
### Known issues
- The service talks to exactly one configured `peer`. The multi-peer
allowlist the fleet-fixes spec describes isn't wired up (F3).
- Neither host flushes on a timer, and tines-ros2 never flushes or trims
(F18).
## 0.3.0 — 2026-10-18 ## 0.3.0 — 2026-10-18
### Breaking ### Breaking
+167 -31
View File
@@ -21,12 +21,15 @@ hear each other.
- [Observations](#observations) - [Observations](#observations)
- [The exchange protocol](#the-exchange-protocol) - [The exchange protocol](#the-exchange-protocol)
- [Addressed delivery](#addressed-delivery) - [Addressed delivery](#addressed-delivery)
- [Receipts](#receipts)
- [Usage](#usage) - [Usage](#usage)
- [Writing a transport](#writing-a-transport) - [Writing a transport](#writing-a-transport)
- [Writing a store](#writing-a-store) - [Writing a store](#writing-a-store)
- [The simulator](#the-simulator) - [The simulator](#the-simulator)
- [Running on a robot](#running-on-a-robot)
- [Crate layout](#crate-layout) - [Crate layout](#crate-layout)
- [Building and CI](#building-and-ci) - [Building and CI](#building-and-ci)
- [Security](#security)
- [Where to read more](#where-to-read-more) - [Where to read more](#where-to-read-more)
## Design at a glance ## Design at a glance
@@ -43,6 +46,8 @@ hear each other.
defensively; malformed bytes are dropped. defensively; malformed bytes are dropped.
- **The application sets the pace.** `Node::poll` emits at most one frame per - **The application sets the pace.** `Node::poll` emits at most one frame per
call, so duty cycle and airtime are the application's call. call, so duty cycle and airtime are the application's call.
- **No trust layer yet.** Anyone in radio range can read and inject. See
[Security](#security).
## Architecture ## Architecture
@@ -109,9 +114,11 @@ saturates rather than underflowing.
### Sequence numbers must survive reboots ### Sequence numbers must survive reboots
`seq` is part of the id, so a robot must not reuse one for different content. `seq` is part of the id, so a robot must not reuse one for different content.
Persist `Node::next_seq()` and pass it back as `first_seq`, typically as Pass `Node::new` a `first_seq` that is higher than any seq used before,
`boot_count << 32 | counter`. A repeated `seq` with identical content simply typically `boot_count << 32`, with a boot counter persisted across reboots.
dedupes (`insert` returns `Ok(false)`). `tines-store`'s `LogStore` keeps that counter for you: seed the node with
`store.first_seq()`. A repeated `seq` with identical content simply dedupes
(`insert` returns `Ok(false)`).
## The exchange protocol ## The exchange protocol
@@ -169,8 +176,16 @@ classDiagram
count: u8 count: u8
data: bytes data: bytes
} }
class Ack {
from: NodeId
ids: ≤90 ObservationIds
}
``` ```
A transport that binds a connection to a peer's identity can check a frame's
claimed sender with `tines_core::wire::frame_sender`, without decoding the
whole frame.
### Fragmentation ### Fragmentation
An observation that doesn't fit the transport's MTU is split into up to 64 An observation that doesn't fit the transport's MTU is split into up to 64
@@ -217,9 +232,9 @@ flowchart TD
Most observations are broadcasts: everyone gets everything. An observation can Most observations are broadcasts: everyone gets everything. An observation can
also be **addressed** to one robot with `Node::observe_to`, e.g. a tasking from also be **addressed** to one robot with `Node::observe_to`, e.g. a tasking from
a base station. Addressed messages travel by store-and-forward: any robot that a base station. Addressed messages travel along a **custody chain**: a message
meets a carrier becomes a carrier too, until the message reaches its recipient has one custodian at a time, and each handoff is confirmed by a receipt before
or its lifetime runs out. the old custodian lets go.
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
@@ -229,28 +244,60 @@ sequenceDiagram
participant R3 as Robot 3 (recipient) participant R3 as Robot 3 (recipient)
Base->>Base: observe_to(R3, lifetime 60 s, "task", …) Base->>Base: observe_to(R3, lifetime 60 s, "task", …)
Note over Base,R1: contact Note over Base,R1: contact
Base->>R1: Obs to=R3 (epidemic forward) Base->>R1: Obs to=R3 (forward)
R1->>Base: Ack
Note over Base: releases its copy:<br/>no longer offered to anyone
Note over R1,R2: later contact Note over R1,R2: later contact
R1->>R2: Obs to=R3 (epidemic forward) R1->>R2: Obs to=R3 (forward)
R2->>R1: Ack
Note over R2,R3: later contact Note over R2,R3: later contact
R2->>R3: Obs to=R3 (delivery) R2->>R3: Obs to=R3 (delivery)
R3->>R2: Ack
Note over R3: shows up in R3's belief() Note over R3: shows up in R3's belief()
Note over R1,R2: carriers drop it once age > lifetime Note over Base,R3: anyone still holding it drops it once age > lifetime
``` ```
- A custodian offers the message to the recipient when it meets it
(**delivery**), and otherwise to any peer it meets (**forwarding**).
Delivery is preferred. Custody and broadcast take turns for each peer, so
neither can starve the other.
- The receiver acks only once it has stored the message. When the ack comes
back, the sender **releases** its copy: the copy stays in its store until
it expires or is trimmed, but is never offered again.
- If an ack is lost, the sender keeps offering the message, so a message can
briefly have two custodians. That costs airtime, never the message. A
custodian that fails after handing the message on does lose it; that's
accepted.
- Addressed messages **never appear in a summary**, so their existence doesn't - Addressed messages **never appear in a summary**, so their existence doesn't
leak to unrelated robots through gossip. leak to unrelated robots through gossip.
- **Delivery** (the peer is the recipient) is preferred over **forwarding**.
Custody and broadcast take turns for each peer, so neither can starve the
other.
- **Bounce suppression:** within one contact, a robot never offers a message - **Bounce suppression:** within one contact, a robot never offers a message
back to the peer it came from, or to a peer that already has it. back to the peer it came from, or to a peer that already has it.
- **Expiry is local and silent.** A message older than its `lifetime_ms` is - **Expiry is local and silent.** A message older than its `lifetime_ms` is
never forwarded and never stored. There are no acks and no cancellation never forwarded and never stored. There are no end-to-end delivery reports
messages. and no cancellation messages.
- `belief()` sees broadcasts and messages addressed *to me*. Messages carried - `belief()` sees broadcasts and messages addressed *to me*. Messages carried
for others exist only to be forwarded. for others exist only to be forwarded.
## Receipts
Each robot acknowledges what it receives with `Ack` frames, sent as it goes
during a contact (up to 90 ids per frame), for both kinds of observation:
| Kind | What an ack means to the sender |
|-----------|-------------------------------------------------------------------------|
| addressed | custody has moved: release my copy |
| broadcast | confidence only: "peer P has observation X"; nothing changes in storage |
An ack is accepted only from a peer the id was actually sent to during the
current contact. A robot never acks an observation it failed to store (for
example, because its store is full), and never acks an addressed duplicate
it has already released, since that would hand custody back to a robot that
no longer carries the message.
Broadcast confirmations surface as `ConfidenceEvent { peer, id }` from
`Node::drain_events`. The event queue is bounded (64): events are telemetry,
and losing one never loses data.
## Usage ## Usage
`tines-core` exposes a single application-facing facade, `Node<S, T>`, generic `tines-core` exposes a single application-facing facade, `Node<S, T>`, generic
@@ -260,18 +307,13 @@ over any `Store` and `Transport`:
use tines_core::{LatestWins, MemStore, Node, NodeId, TrimPolicy}; use tines_core::{LatestWins, MemStore, Node, NodeId, TrimPolicy};
let me: NodeId = [7; 16]; let me: NodeId = [7; 16];
let boot_count = 0u64; let boot_count = 0u64; // persisted and incremented on every boot
let counter = 3u64;
let store = MemStore::new(); let store = MemStore::new();
let transport = /* any Transport implementation */; let transport = /* any Transport implementation */;
// first_seq must survive reboot: boot_count << 32 | counter // first_seq must not repeat across reboots. With tines-store's LogStore,
let mut node: Node<MemStore, _> = Node::new( // pass store.first_seq() instead.
me, let mut node: Node<MemStore, _> = Node::new(me, boot_count << 32, store, transport);
boot_count << 32 | counter,
store,
transport,
);
// Record something you saw. observed_at is an optional absolute clock // Record something you saw. observed_at is an optional absolute clock
// (ms since epoch); `now` is a local monotonic Tick on every call. // (ms since epoch); `now` is a local monotonic Tick on every call.
@@ -291,6 +333,11 @@ let stored = node.poll(500);
// (e.g. the youngest observation wins). // (e.g. the youngest observation wins).
let belief = node.belief(b"door", &LatestWins, 500); let belief = node.belief(b"door", &LatestWins, 500);
// Which peers have confirmed holding which of your broadcasts.
for event in node.drain_events() {
println!("peer {:?} has {:?}", event.peer, event.id);
}
// Keep the store bounded. // Keep the store bounded.
node.trim( node.trim(
&TrimPolicy { &TrimPolicy {
@@ -370,11 +417,15 @@ flash or SQLite backend implements the `Store` trait:
| `for_each_id` | every stored id, any order | | `for_each_id` | every stored id, any order |
| `for_each_broadcast_id` | ids with `to == None`; these feed the summary | | `for_each_broadcast_id` | ids with `to == None`; these feed the summary |
| `for_each_addressed_id` | ids with `to == Some(_)`; scanned for custody on every poll | | `for_each_addressed_id` | ids with `to == Some(_)`; scanned for custody on every poll |
| `release` | mark an id as handed off, so it's never offered again; a no-op if the id isn't stored |
| `is_released` | whether an id has been released; a re-inserted observation starts un-released |
| `for_each_topic` | age-adjusted observations whose topic starts with a prefix | | `for_each_topic` | age-adjusted observations whose topic starts with a prefix |
| `trim` | apply a `TrimPolicy` (per-topic, then age, then count); return how many were removed | | `trim` | apply a `TrimPolicy` (per-topic, then age, then count); return how many were removed |
The store must remember when each observation was inserted (`stored_at`, in The store must remember when each observation was inserted (`stored_at`, in
the caller's `Tick`s) so it can report current ages. the caller's `Tick`s) so it can report current ages. `insert` may return
`Err(StoreError::Full)`; the exchange then doesn't ack, so the sender keeps
custody.
`for_each_addressed_id` has a default implementation that visits every id, `for_each_addressed_id` has a default implementation that visits every id,
which is correct but makes the exchange read every broadcast observation on which is correct but makes the exchange read every broadcast observation on
@@ -420,10 +471,72 @@ check that:
- no store depends on the order of contacts; - no store depends on the order of contacts;
- a store only ever grows during a run (union is monotone); - a store only ever grows during a run (union is monotone);
- garbage frames never panic and never change a store; - garbage frames never panic and never change a store;
- addressed messages reach their recipient along a connected path. - an unexpired addressed message always has a holder, a custodian or its
recipient, including when some robots' stores are full.
`tines-sim/tests/relay.rs` adds scripted relay scenarios, including a message `tines-sim/tests/relay.rs` adds scripted relay scenarios: a message relayed
that expires before it reaches its recipient and is never stored. down a custody chain to its recipient, one that expires before it gets there
and is never stored, and a released duplicate that isn't acked.
## Running on a robot
`tines-core` is only the library. The host crates put it on a Linux robot
(a Pi, or a Jetson running ROS2):
```mermaid
flowchart LR
subgraph robot["One robot"]
direction TB
clients(["IPC clients"]) -- "Unix socket,<br/>length-prefixed JSON" --> svc["tines<br/>(service)"]
ros(["ROS2 nodes"]) -- "/tines/… topics" --> rosnode["tines-ros2<br/>(TinesNode)"]
svc & rosnode --> core["tines-core<br/>Node"]
core --> log[("tines-store<br/>LogStore")]
core --> bt["tines-bt<br/>BtTransport"]
end
bt <-- "BLE L2CAP" --> other(["other robots"])
```
A robot runs either the service or the ROS2 node, not both: each owns its
`Node` and its log.
- **`tines-store`**: `LogStore`, an append-only log file.
- Each record is checksummed (CRC-32C). Recovery skips a corrupt record and
truncates only a torn tail.
- Releases are written as they happen.
- A boot counter provides `first_seq()`.
- `flush()` writes a clock marker, so ages carry across a restart, and
fsyncs the log.
- The file is created `0600` and locked, so only one process can open it.
- **`tines-bt`**: BLE over BlueZ, using L2CAP SeqPacket channels (one
frame per packet).
- Every robot advertises its `NodeId` and scans. The smaller `NodeId`
dials and the larger accepts.
- A connection is bound to the advertised `NodeId`, and frames claiming
any other sender are dropped.
- Connections produce `Contact` and `Lost` events. See
[`tines-bt/README.md`](tines-bt/README.md).
- **`tines`**: the standalone service, for robots without ROS2.
- It runs the exchange on a poll loop and answers `Observe`, `Belief`,
`Events` and `Status` requests on a Unix socket.
- It's configured by a JSON file: `node_id`, `peer`, `log_path`,
`socket_path` and `poll_interval_ms`.
- **`tines-ros2`**: a ROS2 node.
- It publishes each tines topic under `/tines/` and confidence events on
`/tines/receipts`.
- It turns messages on those topics into observations.
- rclrs is behind the `ros2` feature, so the crate builds without a ROS2
toolchain.
The hosts use a monotonic millisecond clock that starts at 0 when the
process does. Neither host sets `observed_at` yet.
**Current limits** (tracked in [`THREAT_MODEL.md`](THREAT_MODEL.md)):
- The service talks to one configured `peer`.
- The hosts don't yet flush on a timer.
- The host-hardening work (trim defaults, bounded channels, socket
permissions, ROS2 isolation) hasn't landed.
The two-device BLE check in `tines-bt/README.md` hasn't been recorded yet.
## Crate layout ## Crate layout
@@ -435,7 +548,11 @@ tines/
`alloc` is a default feature; without it, tines-core builds `alloc` is a default feature; without it, tines-core builds
for thumbv7em-none-eabihf (only MemStore needs alloc). for thumbv7em-none-eabihf (only MemStore needs alloc).
tines-sim/ std: the simulator, contact traces, and the convergence / tines-sim/ std: the simulator, contact traces, and the convergence /
order-independence / relay property tests. order-independence / custody property tests.
tines-store/ std: LogStore, the persistent append-only backend.
tines-bt/ std: the BLE transport (BlueZ via bluer).
tines/ std: the standalone service with Unix-socket IPC.
tines-ros2/ std: the ROS2 node (rclrs behind the `ros2` feature).
``` ```
Fixed limits, all compile-time constants in `tines-core/src/consts.rs`: Fixed limits, all compile-time constants in `tines-core/src/consts.rs`:
@@ -451,6 +568,10 @@ Fixed limits, all compile-time constants in `tines-core/src/consts.rs`:
| `REASSEMBLY_TIMEOUT_MS` | 5000 | partial observation discard time | | `REASSEMBLY_TIMEOUT_MS` | 5000 | partial observation discard time |
| `SEND_QUEUE_CAP` | 256 | ids queued per peer | | `SEND_QUEUE_CAP` | 256 | ids queued per peer |
| `MAX_FRAME` | 1500 | largest frame (and MTU) supported | | `MAX_FRAME` | 1500 | largest frame (and MTU) supported |
| `ACK_IDS_CAP` | 90 | ids in one `Ack` frame |
| `EVENT_QUEUE_CAP` | 64 | confidence events awaiting `drain_events`|
| `BCAST_SENT_CAP` | 256 | broadcast sends remembered per peer, to validate acks |
| `ACK_BUFFER_CAP` | 256 | received ids waiting to be acked, per kind |
## Building and CI ## Building and CI
@@ -465,13 +586,28 @@ sh scripts/ci.sh
GitHub Actions runs the same script on every push to `master` and on every GitHub Actions runs the same script on every push to `master` and on every
pull request. pull request.
## Security
tines has **no trust layer yet**, by design. Anyone in radio range can read
everything, inject observations under any identity, and take custody of
addressed messages. That's acceptable only where the radio environment is
trusted. Signatures are planned for 1.1, with room for them reserved in the
1.0 wire format.
[`THREAT_MODEL.md`](THREAT_MODEL.md) has the full assessment: trust
boundaries, adversaries, findings, and which spec fixes each one.
## Where to read more ## Where to read more
The specs are authoritative; the plans argue from them. The specs are authoritative; the plans argue from them.
- **Core design:** `docs/superpowers/specs/2026-10-16-tines-design.md` - **Core design:** `docs/superpowers/specs/2026-10-16-tines-design.md`
- **Addressed delivery:** `docs/superpowers/specs/2026-10-18-tines-addressed-delivery.md` - **Addressed delivery:** `docs/superpowers/specs/2026-10-18-tines-addressed-delivery.md`
(its epidemic forwarding is superseded by the fleet spec's custody chain)
- **Fleet deployment:** `docs/superpowers/specs/2026-10-18-tines-fleet-deployment-design.md`
- **Fleet fixes:** `docs/superpowers/specs/2026-10-19-tines-fleet-fixes.md`
- **Host hardening (next for the hosts):** `docs/superpowers/specs/2026-10-19-tines-host-hardening.md`
- **tines-core 1.0 (next for the core):** `docs/superpowers/specs/2026-10-18-tines-1-0-design.md`
- **Threat model:** [`THREAT_MODEL.md`](THREAT_MODEL.md)
- **Plans:** `docs/superpowers/plans/` - **Plans:** `docs/superpowers/plans/`
- **Changes:** [`CHANGELOG.md`](CHANGELOG.md)
Not yet built (a second plan): `tines-udp`, `tines-sqlite`, `tines-cli`, and
`tines-serial`.
+122 -39
View File
@@ -3,13 +3,16 @@
- **Assessed:** `master` at `3b1a644` (tines-core / tines-store / tines-bt / - **Assessed:** `master` at `3b1a644` (tines-core / tines-store / tines-bt /
tines-ros2 / tines 0.3.0, fleet-deployment milestone mostly landed) tines-ros2 / tines 0.3.0, fleet-deployment milestone mostly landed)
- **Date:** 2026-10-19 - **Date:** 2026-10-19
- **Status:** first version. This is a living document, not a spec: update - **Updated:** 2026-10-20 at `89041ea`, after the fleet fixes landed. The
it when the code or the trust assumptions change. findings keep their original text; each fixed one ends with a **Fixed**
or **Partly fixed** line, and §6 adds F18, found in the re-check.
- **Status:** living document.It's not a spec: update it when the code or the
trust assumptions change.
- **Related:** the 1.0 spec (`docs/superpowers/specs/2026-10-18-tines-1-0-design.md`), - **Related:** the 1.0 spec (`docs/superpowers/specs/2026-10-18-tines-1-0-design.md`),
especially §2a (signature slot) and §2b (age and the 1.1 threat model). especially §2a (signature slot) and §2b (age and the 1.1 threat model).
Every finding below was checked against the source; `file:line` references Every finding below was checked against the source; `file:line` references
are to `3b1a644`. are to `3b1a644` unless a line says otherwise.
## 1. Summary ## 1. Summary
@@ -43,10 +46,22 @@ Recommended order: fix F1–F5 in the fleet milestone (they're bugs), harden
the hosts (F10–F12) before any robot leaves the lab, and settle the 1.1 the hosts (F10–F12) before any robot leaves the lab, and settle the 1.1
threat questions (§8) before designing signing. threat questions (§8) before designing signing.
**Resolved 2026-10-19** (the fleet-fixes milestone, through `7df9f41`): **Fleet fixes, 2026-10-19** (through `89041ea`):
F1 `2738de4` · F2 `d98895b` · F3 `7df9f41` · F4 `848721a` ·
F5 `4718b6f`+`24b5a78` · F17 `6e2dc6f`. F6 and F8–F14 remain — F6/F8/F9/F13 | # | Status | Commit |
are in the 1.0 spec §10; the rest in the host-hardening spec. |---|---|---|
| F1 | Fixed | `2738de4` |
| F2 | Fixed | `d98895b` |
| F3 | **Partly fixed**: framing, roles, binding and `Contact`/`Lost` are in, but the host is still limited to one configured peer (see F3) | `7df9f41` |
| F4 | **Partly fixed**: seqs and releases are durable, but flushing isn't on a schedule (F18) | `848721a` |
| F5 | Fixed | `4718b6f`, `24b5a78` |
| F17 | Fixed | `6e2dc6f` |
Still open:
- F6, F8, F9 and F13: the 1.0 spec §10.
- F10–F12: the host-hardening spec.
- F7 and F14: 1.1.
- F18 (new) and the rest of F3: host work, not yet specced (§10).
## 2. System and trust boundaries ## 2. System and trust boundaries
@@ -72,9 +87,9 @@ flowchart LR
| Boundary | What crosses it | Who can be on the other side today | | Boundary | What crosses it | Who can be on the other side today |
|---|---|---| |---|---|---|
| **B1 radio** | Advertisements (NodeId in clear), L2CAP frames: Summary, Obs, Frag, Ack | Any BLE device in range. There's no pairing, encryption or allowlist. | | **B1 radio** | Advertisements (NodeId in clear), L2CAP frames: Summary, Obs, Frag, Ack | Any BLE device in range. There's no pairing or encryption. Since `7df9f41` a connection is bound to an advertised NodeId on an allowlist, but advertisements are unauthenticated, so anyone can advertise an allowed NodeId. |
| **B2 local IPC / ROS2** | Observations in, beliefs/payloads and events out | Any local user who can reach the socket (permissions come from umask); any participant on the ROS2 DDS domain (no SROS2). | | **B2 local IPC / ROS2** | Observations in, beliefs/payloads and events out | Any local user who can reach the socket (permissions come from umask); any participant on the ROS2 DDS domain (no SROS2). |
| **B3 storage** | The observation log, released set, `next_seq` | Anyone with write access to the log file (mode from umask). | | **B3 storage** | The observation log: observations, releases, the boot counter | Anyone with write access to the log file. Since `4718b6f` the file is created `0600` (its directory `0700` when the store creates it) and locked against a second opener. |
| Core ↔ transport | `Addr`, raw frames | Trusted code. Core relies on the transport to bind `Addr` to a NodeId (AGENTS.md: "core never maps Addr to NodeId"). | | Core ↔ transport | `Addr`, raw frames | Trusted code. Core relies on the transport to bind `Addr` to a NodeId (AGENTS.md: "core never maps Addr to NodeId"). |
## 3. Assets ## 3. Assets
@@ -110,16 +125,19 @@ in software), compromise of BlueZ/the kernel, and supply chain.
## 5. Security properties: claimed vs actual ## 5. Security properties: claimed vs actual
| Property | Claimed (specs/AGENTS.md) | Actual at `3b1a644` | The third column is the original assessment. The fourth is the state after
|---|---|---| the fleet fixes.
| No panic on untrusted input (core) | Hard constraint | **Holds** as far as reviewed: saturating age arithmetic, guarded shifts and indexing, `expect`s unreachable from wire input (exchange.rs:437, 1092; frag.rs:147). The sim's `garbage_frames_never_panic` covers random bytes only (§9). |
| Malformed frames dropped | Hard constraint | **Holds** in core (wire.rs:64-69). The 1.0 spec adds reporting. | | Property | Claimed (specs/AGENTS.md) | Actual at `3b1a644` | At `89041ea` |
| Bounded memory in core | Fixed-capacity buffers | **Holds** for the exchange (§6 table). **Doesn't hold for the system**: MemStore and LogStore are unbounded without `trim`, and no host calls `trim` (F10). | |---|---|---|---|
| Custody: ≥ 1 holder (no carrier failure) | Fleet spec, sim property | **Breaks without an attacker** when a store is full (F1). Breaks trivially under A1 (F7). | | No panic on untrusted input (core) | Hard constraint | **Holds** as far as reviewed: saturating age arithmetic, guarded shifts and indexing, `expect`s unreachable from wire input (exchange.rs:437, 1092; frag.rs:147). The sim's `garbage_frames_never_panic` covers random bytes only (§9). | Unchanged. |
| Frames bound to the advertised NodeId | Fleet spec ("frames whose `from` differs … are dropped at the transport") | **Doesn't hold** (F3, F6). | | Malformed frames dropped | Hard constraint | **Holds** in core (wire.rs:64-69). The 1.0 spec adds reporting. | Unchanged; 1.0 adds reporting. |
| Released messages not re-forwarded after reboot | Fleet spec | **Doesn't hold**: release flags are never flushed (F4). Safe direction (duplicate custodian), but wasteful. | | Bounded memory in core | Fixed-capacity buffers | **Holds** for the exchange (§6 table). **Doesn't hold for the system**: MemStore and LogStore are unbounded without `trim`, and no host calls `trim` (F10). | Unchanged in practice: the service calls `trim` with an empty default policy, tines-ros2 never does (F10, F18). |
| `next_seq` persisted across reboots | Core contract | **Doesn't hold** in either host (F4). | | Custody: ≥ 1 holder (no carrier failure) | Fleet spec, sim property | **Breaks without an attacker** when a store is full (F1). Breaks trivially under A1 (F7). | Holds without an attacker (F1 fixed, with a sim property). Still breaks under A1 (F7). |
| Age continues across reboot | Fleet spec | Partly. Hosts open the store at tick 0, and the service's tick is wall-clock *seconds* (F17). | | Frames bound to the advertised NodeId | Fleet spec ("frames whose `from` differs … are dropped at the transport") | **Doesn't hold** (F3, F6). | **Holds at the transport** for tines-bt (F3), against unauthenticated adverts. Core still re-reads `from` per frame (F6). |
| Released messages not re-forwarded after reboot | Fleet spec | **Doesn't hold**: release flags are never flushed (F4). Safe direction (duplicate custodian), but wasteful. | **Holds**: `Release` records are appended at once (not fsynced until the next flush, so a power cut can still lose one). |
| `next_seq` persisted across reboots | Core contract | **Doesn't hold** in either host (F4). | **Holds**: a persisted boot counter seeds `first_seq` in both hosts. |
| Age continues across reboot | Fleet spec | Partly. Hosts open the store at tick 0, and the service's tick is wall-clock *seconds* (F17). | Partly. Monotonic ms (F17 fixed) and clock markers fold age across a restart, but only up to the last flush, which may be long ago or never (F18). |
## 6. Findings ## 6. Findings
@@ -192,8 +210,20 @@ for a poll loop, `docs/superpowers/plans/2026-10-18-tines-fleet-deployment.md:20
- Decode the advertised NodeId and bind the connection to it. - Decode the advertised NodeId and bind the connection to it.
- Drop frames whose `from` differs from the bound NodeId. - Drop frames whose `from` differs from the bound NodeId.
- Emit `Contact`/`Lost`. - Emit `Contact`/`Lost`.
- **Fixed:** `7df9f41` (SeqPacket framing, advertise+scan roles, UUID-checked - **Partly fixed:** `7df9f41` (SeqPacket framing, advertise+scan roles,
NodeId binding, `frame_sender` from-check, `Contact`/`Lost`, multi-peer). UUID-checked NodeId binding, `frame_sender` from-check, `Contact`/`Lost`).
Re-checked at `89041ea`:
- The fleet-fixes spec's allowlist (empty = any tines advertiser) and
multi-peer support aren't reachable. The service config still takes one
required `peer`. `BluerRadio::new` builds the allowlist as exactly
`{peer}`, and `BtTransport::poll` drops every frame and event whose
address isn't that peer (tines-bt/src/transport.rs:186-202, 274). The
radio loop can hold several connections, but only one is ever used, so
each robot exchanges with one configured partner.
- Inbound and outbound channels are still unbounded (F10).
- No BLE security level is requested (unchanged; 1.1).
- No two-radio check of the SeqPacket path is recorded yet.
tines-bt/README.md gives the procedure but no result.
**Later (1.1):** authenticate the link (spec §2b). Advertised NodeIds are **Later (1.1):** authenticate the link (spec §2b). Advertised NodeIds are
unauthenticated, so the binding only becomes meaningful once there's a unauthenticated, so the binding only becomes meaningful once there's a
@@ -221,7 +251,9 @@ handshake.
`Node` from it. `Node` from it.
- Flush on release and on a timer. - Flush on release and on a timer.
- Add a host-level restart test. - Add a host-level restart test.
- **Fixed:** `848721a`. - **Partly fixed:** `848721a`. The boot counter seeds `first_seq` in both
hosts, and releases are appended as records immediately. Flushing isn't
on a schedule, though; see F18.
### F5 — Log recovery can destroy good data (Medium, A0/A5) ### F5 — Log recovery can destroy good data (Medium, A0/A5)
@@ -290,6 +322,8 @@ custody of every addressed message in range and drop it.
frame, and removed only on `Lost` (exchange.rs:674-708). A full table drops frame, and removed only on `Lost` (exchange.rs:674-708). A full table drops
all frames from new addresses. There's no idle eviction. With BLE never all frames from new addresses. There's no idle eviction. With BLE never
emitting `Lost` (F3), entries effectively never leave on real hardware. emitting `Lost` (F3), entries effectively never leave on real hardware.
(Since `7df9f41` tines-bt emits `Lost`, so honest peers leave; spoofed
addresses on other transports still can't be evicted.)
Eight spoofed addresses lock a robot out of exchange until restart. Eight spoofed addresses lock a robot out of exchange until restart.
**Fix:** idle-timeout eviction in core (a local constant), and real **Fix:** idle-timeout eviction in core (a local constant), and real
@@ -315,6 +349,10 @@ re-queues what was already sent.
- `MemStore` and `LogStore` are bounded only by `Node::trim`. Neither host - `MemStore` and `LogStore` are bounded only by `Node::trim`. Neither host
calls it. The log also never compacts (log.rs: append-only, and every calls it. The log also never compacts (log.rs: append-only, and every
flush appends the full released list). flush appends the full released list).
- *At `89041ea`:* the service calls `trim` with `TrimPolicy::default()`,
which removes nothing; tines-ros2 never calls it. Releases are now one
small record each rather than a growing list, but the log still never
compacts.
- Unbounded channels: tines-bt's inbound and outbound `mpsc` - Unbounded channels: tines-bt's inbound and outbound `mpsc`
(transport.rs:207-208), and tines-ros2's inbound `mpsc` (node.rs:152). (transport.rs:207-208), and tines-ros2's inbound `mpsc` (node.rs:152).
- The IPC server allocates up to 1 MiB per request frame. - The IPC server allocates up to 1 MiB per request frame.
@@ -433,6 +471,37 @@ full preimage, not the id.
everywhere in the host, and open the store with the same clock. everywhere in the host, and open the store with the same clock.
- **Fixed:** `6e2dc6f`. - **Fixed:** `6e2dc6f`.
### F18 — Hosts don't flush on a schedule (Medium, A0)
*Found in the re-check at `89041ea`.* `LogStore::flush` writes the clock
marker that carries age across a restart, and it's the only `fsync` for
inserts and trims (log.rs module docs, "Durability contract").
- The service runs `trim` and `flush` only after `POLL_CAP` (64) polls that
each stored something new (tines/src/ipc.rs:199-217). The counter
advances only while observations arrive, so a robot that is mostly
offline, the normal case, may never flush.
- tines-ros2 never calls `flush` or `trim`.
- Even when it runs, `flush` writes `LogStore`'s own tick as the clock
marker, and that tick advances only when something new is inserted
(tines-store/src/log.rs:698). On a quiet robot the marker is stale, and
the time since the last insert is lost.
- **Effect:**
- After a restart, every stored observation's age is folded only up to the
last clock marker. Time since that marker is lost, so facts look fresher
than they are and addressed messages outlive their `lifetime_ms`, by up
to the whole previous uptime. That is F17's effect again, by another
route.
- On a power cut, observations inserted since the last flush can be lost.
That includes a robot's own observations, though their seqs aren't
reused.
- **Fix:** give flush the caller's `now` (proposed for 1.0: `Store::flush(now)`
and `Node::flush(now)`, replacing `Node::store_mut`), and
flush and trim on a wall-time schedule (every
`flush_interval_ms`, from the host's monotonic clock) in both hosts, and
on clean shutdown. The fleet-fixes spec asked for "a scheduled flush". The
trim defaults belong to host hardening (F10).
## 7. Threats by STRIDE, mapped to findings ## 7. Threats by STRIDE, mapped to findings
| | Threat | Status today | Findings | Planned mitigation | | | Threat | Status today | Findings | Planned mitigation |
@@ -477,32 +546,44 @@ The sim covers honest-node custody properties and random garbage frames
- well-formed malicious frames: forged acks, spoofed `from`/`observer`, - well-formed malicious frames: forged acks, spoofed `from`/`observer`,
NodeId changes mid-contact; NodeId changes mid-contact;
- a store returning `Full` (F1); - ~~a store returning `Full` (F1)~~ (covered since `2738de4`: a unit test
and the full-store custody sim property);
- summary floods and peer-table exhaustion (F8, F9); - summary floods and peer-table exhaustion (F8, F9);
- reassembly abuse; - reassembly abuse;
- `LogStore` corruption in the middle, an oversized `Meta`, concurrent - a full disk (`LogStore` corruption in the middle, oversized records,
openers, a full disk, and restart and seq continuity (F4, F5); concurrent openers, and restart and seq continuity are covered since
- anything in the hosts: IPC abuse, ROS2 injection and self-echo `848721a` and `4718b6f`);
(F11, F12). - a quiet host restarting without having flushed (F18);
- most of the hosts: IPC abuse, ROS2 injection and self-echo (F11, F12).
The service now has a two-instance exchange test (`d98895b`), and
tines-bt has mock-radio tests for framing, binding and contact events
(`7df9f41`).
The 1.0 fuzz harness (spec §7) should take an *adversarial peer* mode, not The 1.0 fuzz harness (spec §7) should take an *adversarial peer* mode, not
just random bytes: structured frames with chosen ids and identities. just random bytes: structured frames with chosen ids and identities.
## 10. Where each finding is being fixed ## 10. Where each finding is being fixed
| Findings | Where | Spec | | Findings | Where | Spec | Status |
|---|---|---| |---|---|---|---|
| F1–F5, F17 | Fleet milestone (M3), now | `docs/superpowers/specs/2026-10-19-tines-fleet-fixes.md` | | F1, F2, F5, F17 | Fleet milestone (M3) | `docs/superpowers/specs/2026-10-19-tines-fleet-fixes.md` | Fixed (§1) |
| F10–F12 | Host hardening, before a robot leaves the lab | `docs/superpowers/specs/2026-10-19-tines-host-hardening.md` | | F3, F4 | Fleet milestone (M3) | same | Partly fixed; the rest has no spec yet |
| F6, F8, F9, F13 | tines-core 1.0 | `docs/superpowers/specs/2026-10-18-tines-1-0-design.md` §10 | | F18 | Host work, with or before host hardening | none yet | Open |
| F7, F14, §8 | 1.1 (threat model, link authentication, signing) | 1.0 spec §2a, §2b | | F10–F12 | Host hardening, before a robot leaves the lab | `docs/superpowers/specs/2026-10-19-tines-host-hardening.md` | Open |
| F15, F16 | Accepted for now | — | | F6, F8, F9, F13 | tines-core 1.0 | `docs/superpowers/specs/2026-10-18-tines-1-0-design.md` §10 | Open |
| F7, F14, §8 | 1.1 (threat model, link authentication, signing) | 1.0 spec §2a, §2b | Open |
| F15, F16 | Accepted for now | — | — |
Mark each finding fixed here, with its commit, when it lands. Mark each finding fixed here, with its commit, when it lands.
The remaining F3 work (one configured peer) and F18 are host-only. Neither
blocks the tines-core 1.0 work, but both block a fleet of more than two
robots.
## 11. Recommendations, in order ## 11. Recommendations, in order
1. **Fleet milestone bugs:** 1. **Fleet milestone bugs** (done 2026-10-19, except the F3 and F4
remainders):
- F1 (ack only what was stored). - F1 (ack only what was stored).
- F2 (poll loop). - F2 (poll loop).
- F3 (framing, listener, NodeId binding, Contact/Lost). - F3 (framing, listener, NodeId binding, Contact/Lost).
@@ -511,20 +592,22 @@ Mark each finding fixed here, with its commit, when it lands.
locking). locking).
- F17 (monotonic ms clock). - F17 (monotonic ms clock).
- Each with a regression test. - Each with a regression test.
2. **Host hardening before any robot leaves the lab:** 2. **Host follow-ups:** the F3 multi-peer allowlist and F18 scheduled
flush. Small, and needed before more than two robots share a fleet.
3. **Host hardening before any robot leaves the lab:**
- F10 (trim, bounded channels, compaction). - F10 (trim, bounded channels, compaction).
- F11 (socket permissions, peer creds, timeouts). - F11 (socket permissions, peer creds, timeouts).
- F12 (ignore own publications, SROS2 guidance, rate limits). - F12 (ignore own publications, SROS2 guidance, rate limits).
3. **Core hardening (can ride with 1.0):** 4. **Core hardening (rides with 1.0):**
- F8 (idle eviction). - F8 (idle eviction).
- F9 (summary rate limit, no re-queue). - F9 (summary rate limit, no re-queue).
- F13 (event drop-oldest, purge acks on peer loss, reject trailing - F13 (event drop-oldest, purge acks on peer loss, reject trailing
bytes). bytes).
- F6 (refuse NodeId change within a contact). - F6 (refuse NodeId change within a contact).
4. **1.1:** 5. **1.1:**
- Write the full threat model from §4/§8. - Write the full threat model from §4/§8.
- Choose link authentication. - Choose link authentication.
- Add signing per spec §2a (F14, F7). - Add signing per spec §2a (F14, F7).
- Add "replace if better signed" dedupe. - Add "replace if better signed" dedupe.
5. **Later:** confidentiality and rotating rendezvous identifiers (F15), 6. **Later:** confidentiality and rotating rendezvous identifiers (F15),
only if the deployment needs them. only if the deployment needs them.
@@ -1,10 +1,11 @@
# tines-core 1.0 — design # tines-core 1.0 — design
Date: 2026-10-18 Date: 2026-10-18
Status: draft v6. Kyle decided D1–D5 and Q1–Q3 (2026-10-18) and the Status: approved by Kyle (2026-10-20). Kyle decided D1–D5 and Q1–Q3
review decisions R1–R3 and the `observed_at` definition (2026-10-19); (2026-10-18) and the review decisions R1–R3 and the `observed_at`
review fixes applied. v6 adds §10 (core hardening from `THREAT_MODEL.md`) definition (2026-10-19); review fixes applied. v6 added §10 (core hardening
and the adversarial fuzz mode; these additions aren't reviewed yet. from `THREAT_MODEL.md`) and the adversarial fuzz mode; Kyle reviewed and
approved both (2026-10-20).
Builds on: `2026-10-16-tines-design.md`, `2026-10-18-tines-addressed-delivery.md`, Builds on: `2026-10-16-tines-design.md`, `2026-10-18-tines-addressed-delivery.md`,
`2026-10-18-tines-fleet-deployment-design.md` (assumed fully implemented) `2026-10-18-tines-fleet-deployment-design.md` (assumed fully implemented)
+10
View File
@@ -52,6 +52,12 @@ violation).
Up to [`MAX_PEERS`] connections may be live at once, one per bound [`NodeId`]; Up to [`MAX_PEERS`] connections may be live at once, one per bound [`NodeId`];
a second connection to the same [`NodeId`] replaces the first. a second connection to the same [`NodeId`] replaces the first.
**Current limit:** `BluerRadio::new` and `BtTransport::new` still take a single
`peer`. The allowlist is exactly that one `NodeId`, and `BtTransport` drops
frames and events from any other peer, so in practice each node exchanges with
one configured partner. A configurable allowlist (empty = any tines advertiser)
is still to come; see `THREAT_MODEL.md` F3.
## Manual verification ## Manual verification
The SeqPacket framing can't be exercised in CI (it needs two real radios), so The SeqPacket framing can't be exercised in CI (it needs two real radios), so
@@ -64,6 +70,10 @@ verify it by hand with two devices running the `tines` service:
3. send a burst of observations on one device and confirm the other receives 3. send a burst of observations on one device and confirm the other receives
each as a separate, intact frame (no merged or truncated frames). each as a separate, intact frame (no merged or truncated frames).
No result has been recorded yet. Once it's done, note the date, hardware and
BlueZ version here, along with whether SeqPacket worked or the length-prefix
fallback was needed.
[`Transport`]: https://docs.rs/tines-core/latest/tines_core/transport/trait.Transport.html [`Transport`]: https://docs.rs/tines-core/latest/tines_core/transport/trait.Transport.html
[`NodeId`]: https://docs.rs/tines-core/latest/tines_core/observation/struct.NodeId.html [`NodeId`]: https://docs.rs/tines-core/latest/tines_core/observation/struct.NodeId.html
[`Radio`]: crate::Radio [`Radio`]: crate::Radio
+6 -5
View File
@@ -15,16 +15,17 @@
//! ``` //! ```
//! //!
//! Each frame is one of the [`Request`] or [`Response`] enums, tagged by a //! Each frame is one of the [`Request`] or [`Response`] enums, tagged by a
//! `"kind"` field (serde externally-tagged). Frames are capped at 1 MiB; a //! `"kind"` field (serde internally tagged). Frames are capped at 1 MiB. A
//! longer frame is treated as a malformed request and the connection is //! longer length prefix gets an error response, but its body isn't consumed,
//! dropped. There is no framing negotiation — the first byte after the length //! so the connection is out of sync afterwards; the host-hardening spec makes
//! prefix is `"`. //! it close the connection instead (`THREAT_MODEL.md` F11). There is no
//! framing negotiation: the first byte after the length prefix is `{`.
//! //!
//! Observations are content-addressed, so IDs are exchanged as lowercase hex //! Observations are content-addressed, so IDs are exchanged as lowercase hex
//! (the hex form of the 16-byte `ObservationId`). Topics and payloads are //! (the hex form of the 16-byte `ObservationId`). Topics and payloads are
//! passed to `observe` the same way: as hex. //! passed to `observe` the same way: as hex.
//! //!
//! # Sequence numbers //! # Clock
//! //!
//! `now` is the tines tick passed to `observe`/`belief`. The service reads it //! `now` is the tines tick passed to `observe`/`belief`. The service reads it
//! from its own monotonic millisecond [`HostClock`] (an `Instant` captured at //! from its own monotonic millisecond [`HostClock`] (an `Instant` captured at
+3 -3
View File
@@ -4,9 +4,9 @@
//! `bluer`), and serves the IPC ([`tines::serve`]) over a Unix-domain socket. //! `bluer`), and serves the IPC ([`tines::serve`]) over a Unix-domain socket.
//! One instance per device — the typical deployment on robots without ROS2. //! One instance per device — the typical deployment on robots without ROS2.
//! //!
//! The node/peer ids are 32 lowercase hex chars (the 16-byte `NodeId`), and //! The node and peer ids are 32 lowercase hex chars (the 16-byte `NodeId`).
//! The tines sequence numbers are seeded from the LogStore's boot counter, so //! Sequence numbers are seeded from the LogStore's boot counter, so they don't
//! they persist across reboots without a separate config field. //! repeat across reboots and need no config field.
use std::error::Error; use std::error::Error;
use std::path::Path; use std::path::Path;