- CHANGELOG: tines-core 1.0.0 and the hosts' 0.4.0 are separate, unreleased sections. Every core API break is listed (the nine pub(crate) items, the private modules and the frame_sender path, the local constants per decision K, non_exhaustive types, store_mut). The new names (WallClockStore, Node::set_wall_clock, peer_time_estimate, flush, IdentityChanged) are called out. The host section covers tines::trust, the config changes, decision J, snapshots, and the review fixes. - THREAT_MODEL: F6, F8, F9 and F13 are fixed with their commits. F18 is partly fixed: the store side and the flush on first trusted time are done, but timed and shutdown flushes are still open. F19 now records decision J and the plausibility bounds, and what remains. §9, §10 and §11 are updated to match. - README: Dest and sig in the observation table; flush and is_empty in the store table; only the public wire constants are listed; a new "Trusted time" section with the config keys and the systemd ordering; the MSRV and fuzz CI jobs; frame_sender's new path. - AGENTS: 1.0 is implemented but not yet published. The constants list shows the pub(crate) split, PEER_IDLE_MS, MIN_MTU's role and where MAX_FRAGMENTS lives. Notes the hidden conformance module. - tines-bt README: frame_sender's path, and its own MAX_PEERS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
163 lines
8.4 KiB
Markdown
163 lines
8.4 KiB
Markdown
# AGENTS.md — tines
|
||
|
||
Instructions for AI agents working on **tines**: opportunistic shared memory
|
||
for mostly-offline robots. Read this before touching code.
|
||
|
||
## What tines is
|
||
|
||
Small robots collect facts about the world while mostly offline. When two of
|
||
them happen to meet in range, they swap what they know so each one's picture of
|
||
the world improves. No robot depends on receiving anything, but there is no
|
||
reason not to share.
|
||
|
||
`tines` is the library that does this: a store of timestamped-by-age
|
||
observations, a belief layer derived from them, and a delay-tolerant exchange
|
||
protocol over any datagram link. Read the full intent, goals, anti-goals, and
|
||
architecture in the **spec first**:
|
||
|
||
- **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`
|
||
- **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 (implemented on `feat/tines-core-1-0`; publish pending):** `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/`
|
||
|
||
The plans argue from the specs. Where a plan is silent, the **spec rules**.
|
||
Do not edit the specs or the plans on your own initiative — they are design
|
||
history, not source. Change one only to record something Kyle has discussed
|
||
and agreed, and record it as a dated decision in that document. Anything
|
||
else that needs changing in a spec goes to Kyle first.
|
||
`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)
|
||
|
||
```
|
||
memory — "my priors": beliefs per topic, derived from storage under a policy
|
||
storage — append-only log of raw observations; local trimming policy
|
||
comms — contact-driven exchange of observations; transport trait + adapters
|
||
```
|
||
|
||
Only observations cross the wire. Memory is a pure function of storage plus a
|
||
policy and is never transmitted. See the spec's "Architecture" section for the
|
||
`Observation`, `Store`, `BeliefPolicy`, `Transport`, and exchange protocol
|
||
details.
|
||
|
||
## Workflow
|
||
|
||
Changes are TDD: write the failing test → see it fail → implement → see it
|
||
pass → commit.
|
||
|
||
1. Keep the public API small. New public names in `tines-core` (types, trait
|
||
methods, functions) need a reason; call them out when you add them.
|
||
2. Before committing, run the full gate, which is what CI runs:
|
||
`sh scripts/ci.sh` (fmt, clippy and rustdoc with warnings as errors, tests
|
||
with and without `alloc`, and the `thumbv7em-none-eabihf` build).
|
||
3. Use conventional commit messages (`fix(core): …`, `ci: …`, `docs: …`).
|
||
|
||
The `Node` facade is core, not application glue: an application on any target
|
||
should only need to construct a `Node` with its `NodeId`, store, and transport,
|
||
call `poll()` in its loop, and `observe()` when it sees something.
|
||
|
||
## Hard constraints (non-negotiable in `tines-core`)
|
||
|
||
- `tines-core` is `#![no_std]`; `alloc` is a cargo feature (default on). With
|
||
`--no-default-features` it must build for `thumbv7em-none-eabihf`. Only
|
||
`MemStore` may require `alloc`.
|
||
- No async, no threads, no I/O, and no panic on untrusted input in core. Every
|
||
frame from the wire is untrusted: malformed bytes are dropped, never
|
||
unwrapped.
|
||
- Core never maps a transport `Addr` to a `NodeId`; every frame carries the
|
||
sender's `NodeId`.
|
||
- Identity hash covers `(observer, seq, topic, payload, to)` **only**.
|
||
`age_ms`, `observed_at` and `lifetime_ms` are excluded.
|
||
- Addressed observations (`to = Some`) never appear in a summary Bloom
|
||
filter, and an addressed observation past its `lifetime_ms` is never
|
||
stored or forwarded.
|
||
- `first_seq` passed to `Node::new` must not repeat across reboots
|
||
(`boot_count << 32`, with a persisted boot counter; `LogStore::first_seq`
|
||
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
|
||
monotonic ms, supplied on every call).
|
||
- 1.0 trait extension rule: any method added to `Store`, `Transport` or
|
||
`BeliefPolicy` after 1.0 must have a default whose behaviour is safe — it
|
||
may lose performance or telemetry but never data or custody. A method that
|
||
can't have such a default is a 2.0 change (the rule `release` violated in
|
||
0.3.0).
|
||
|
||
### Review Focus (each covered by a test; keep it that way)
|
||
|
||
1. Reused `seq` after reboot → store dedupes, `insert` returns `Ok(false)`.
|
||
2. Oversized `topic`/`payload` at `observe()` → `Err(TooLarge)`, never truncate/panic.
|
||
3. Garbage frames → dropped, no panic, no store change.
|
||
4. Frame from an address with no prior `Contact` → treated as implicit contact.
|
||
5. Tick going backwards → age saturates at stored value, never underflow.
|
||
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`)
|
||
|
||
Split into **wire** (frozen for wire v1 — changing one changes how a v1 peer
|
||
decodes or interprets a frame) and **local** (never crosses the wire; may
|
||
change in a minor release):
|
||
|
||
- **Wire** (public): `WIRE_VERSION=4`, `MAX_TOPIC=64`, `MAX_PAYLOAD=1024`,
|
||
`MAX_FRAME=1500`, `SUMMARY_BYTES=512`, `ACK_IDS_CAP=90`, `MAX_SIG=64`,
|
||
`BLOOM_K=3`. Also wire, but private: `MAX_FRAGMENTS=64`, which lives in
|
||
`tines-core/src/frag.rs`, not `consts.rs`.
|
||
- **Local** (`pub(crate)`, spec decision K: defaults that a later 1.x makes
|
||
configurable): `MAX_PEERS=8`, `SUMMARY_INTERVAL_MS=2000`,
|
||
`REASSEMBLY_TIMEOUT_MS=5000`, `SEND_QUEUE_CAP=256`, `EVENT_QUEUE_CAP=64`,
|
||
`DROP_TABLE_CAP=32`, `BCAST_SENT_CAP=256`, `ACK_BUFFER_CAP=256`,
|
||
`PEER_IDLE_MS=30_000`, and `MIN_MTU=23`. `MIN_MTU` is the lower bound of the
|
||
fragmentation MTU sweep in tests (BLE ATT's default), not the smallest
|
||
usable MTU; `Transport::mtu`'s rustdoc states those floors (85 bytes for
|
||
every valid observation).
|
||
|
||
Match the exact values above. A crate outside core that needs a matching
|
||
number keeps its own constant (as `tines-bt` does for `MAX_PEERS` and
|
||
`MIN_MTU`).
|
||
|
||
The `conformance` feature also compiles a `#[doc(hidden)]`
|
||
`tines_core::conformance` module: byte-level frame encoders and decoders, and
|
||
mirrors of the local limits, for the fuzz harness and tests outside core. It
|
||
isn't supported public API and isn't covered by semver. Don't build on it
|
||
outside the workspace and `fuzz/`.
|
||
|
||
## Crate layout
|
||
|
||
```
|
||
tines/
|
||
tines-core/ no_std core: Observation, Store/Transport/BeliefPolicy traits,
|
||
exchange state machine, Node facade, in-memory store, reference
|
||
policies
|
||
tines-sim/ std: simulator, contact traces, convergence, relay and custody
|
||
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
|
||
|
||
`tines-udp`, `tines-sqlite`, `tines-cli`, and `tines-serial` (from the core
|
||
spec's crate layout) belong to a future plan. Don't start them unless asked.
|