/* * antheos.hpp — Antheos Protocol v1.0 Level 1 * * Pure C++17 implementation. Transport-agnostic stateful codec * for peer-to-peer messaging over any byte stream. * * Copyright (c) 2025-2026 Are Bjørby * SPDX-License-Identifier: MIT */ #pragma once #include #include #include #include #include #include #include #include #include namespace antheos { /* ══════════════════════════════════════════════════════════════════ * Frame — value type holding encoded wire data * ══════════════════════════════════════════════════════════════════ */ class Frame { std::vector data_; public: Frame() = default; Frame(const uint8_t* d, size_t n) : data_(d, d + n) {} explicit Frame(std::vector&& v) : data_(std::move(v)) {} const uint8_t* data() const { return data_.data(); } size_t size() const { return data_.size(); } bool empty() const { return data_.empty(); } explicit operator bool() const { return !data_.empty(); } const uint8_t* begin() const { return data_.data(); } const uint8_t* end() const { return data_.data() + data_.size(); } uint8_t operator[](size_t i) const { return data_[i]; } std::vector& bytes() { return data_; } const std::vector& bytes() const { return data_; } }; /* ══════════════════════════════════════════════════════════════════ * Wire — control characters, word types, encoding/decoding * (v9 spec §4–§6) * ══════════════════════════════════════════════════════════════════ */ namespace wire { /* ── Control Characters (v9 §4.1) ── exactly 7 ── */ inline constexpr uint8_t SOM = 0x02; /* Start of Message */ inline constexpr uint8_t EOM = 0x03; /* End of Message */ inline constexpr uint8_t SOR = 0x04; /* Start of Radix qualifier */ inline constexpr uint8_t SOU = 0x07; /* Start of Unit qualifier */ inline constexpr uint8_t EOW = 0x10; /* End of Word */ inline constexpr uint8_t SOW = 0x12; /* Start of Word */ inline constexpr uint8_t SOB = 0x1A; /* Start of Body */ /* ── Word Types (v9 §6.2) ── */ enum class WordType : uint8_t { Symbol = '!', // 0x21 — Protocol verbs Text = '"', // 0x22 — Plain text Integer = '#', // 0x23 — Integer values Real = '$', // 0x24 — Floating-point Scientific = '%', // 0x25 — Exponential notation Timestamp = '&', // 0x26 — ISO 8601 Blob = '*', // 0x2A — Tail size declaration Duration = '+', // 0x2B — ISO 8601 duration Path = '/', // 0x2F — Routing sequences Logical = '?', // 0x3F — Boolean expressions Id = '@', // 0x40 — Identifiers Message = '~', // 0x7E — Embedded message ref }; /* ── Radix Flags (v9 §6.3) ── */ enum class Radix : uint8_t { None = 0, Binary = 'I', // Base-2 Octal = 'O', // Base-8 Decimal = 'D', // Base-10 Hex = 'H', // Base-16 Base32 = 'U', // Base-32 (duotrigesimal) }; /* ── Unit Flags (v9 §6.4) ── */ enum class Unit : uint8_t { None = 0, Byte = 'B', // 8 bits Word = 'W', // 16 bits Dword = 'D', // 32 bits Qword = 'Q', // 64 bits Megabyte = 'M', // 2^20 bytes Gigabyte = 'G', // 2^30 bytes Terabyte = 'T', // 2^40 bytes }; /* ── Validation ── */ bool is_reserved(uint8_t byte); bool is_valid_word_type(uint8_t byte); bool is_radix_flag(uint8_t byte); bool is_unit_flag(uint8_t byte); /* ── Flag requirement rules (v9 §6.1) ── * * Radix + Unit required: INTEGER, REAL, SCIENTIFIC, BLOB * Radix optional, Unit forbidden: ID * Both forbidden: SYMBOL, PATH, TEXT, LOGICAL, TIMESTAMP, MESSAGE */ bool needs_radix(WordType type); bool needs_unit(WordType type); bool allows_radix(WordType type); /* v9 §6.1, both halves: does this word type permit the flags it carries, and * carry the flags it requires? The one predicate word_encode, word_decode and * the parser all consult (ANTHEOS-WIRE-FLAG-RULES-ASYMMETRIC). */ bool flags_conform(WordType type, Radix radix, Unit unit); /* ── Decoded word ── */ struct DecodedWord { WordType type; Radix radix; Unit unit; std::vector body; explicit operator bool() const { return !body.empty() || type == WordType::Symbol; } }; /* ── Word encoding/decoding — v9 wire format ── * * Wire: [SOW][WT]([SOR][RF])([SOU][UF])[SOB]Body[EOW] */ std::optional> word_encode( WordType type, Radix radix, Unit unit, const uint8_t* body, size_t body_len); std::optional word_decode( const uint8_t* in, size_t in_len); /* ── Convenience encoders ── */ std::optional> encode_symbol(char verb); std::optional> encode_text(std::string_view text); std::optional> encode_id_plain(std::string_view id); std::optional> encode_id_base32(std::string_view id); std::optional> encode_id_decimal(uint32_t value); std::optional> encode_integer( Radix radix, Unit unit, std::string_view value); std::optional> encode_logical(std::string_view expr); std::optional> encode_path(std::string_view path); std::optional> encode_message(std::string_view ref); } // namespace wire /* ══════════════════════════════════════════════════════════════════ * Standard Exception Codes (v9 spec §13, Appendix A) * * Reason strings for the Exception verb (!X). Use with * bus::exception() or Context::exception(). * ══════════════════════════════════════════════════════════════════ */ namespace exc { /* Bus scope */ inline constexpr const char* BID_OVERFLOW = "BID_OVERFLOW"; inline constexpr const char* BID_TIMEOUT = "BID_TIMEOUT"; inline constexpr const char* RELAY_FAILED = "RELAY_FAILED"; inline constexpr const char* PATH_BROKEN = "PATH_BROKEN"; inline constexpr const char* INDEX_INVALID = "INDEX_INVALID"; inline constexpr const char* UNKNOWN_BID = "UNKNOWN_BID"; inline constexpr const char* MALFORMED_FRAME = "MALFORMED_FRAME"; inline constexpr const char* UNSUPPORTED_TYPE = "UNSUPPORTED_TYPE"; /* Service scope */ inline constexpr const char* SERVICE_UNKNOWN = "SERVICE_UNKNOWN"; inline constexpr const char* OFFER_EXPIRED = "OFFER_EXPIRED"; /* Session scope */ inline constexpr const char* SESSION_NOT_FOUND = "SESSION_NOT_FOUND"; inline constexpr const char* SESSION_EXPIRED = "SESSION_EXPIRED"; inline constexpr const char* RESUME_DENIED = "RESUME_DENIED"; inline constexpr const char* MID_OUT_OF_ORDER = "MID_OUT_OF_ORDER"; /* Level 2: Authentication (Appendix A) */ inline constexpr const char* AUTH_FAILED = "AUTH_FAILED"; } // namespace exc /* ══════════════════════════════════════════════════════════════════ * Identity — Base-32, BID/SID generation * (v9 spec §5, §11.2) * ══════════════════════════════════════════════════════════════════ */ namespace id { inline constexpr char BASE32_ALPHABET[] = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; inline constexpr size_t BID_MIN_LEN = 2; inline constexpr size_t BID_MAX_LEN = 16; inline constexpr size_t SID_MIN_LEN = 6; /* 32^6 ≈ 1.07e9 — clear of the short-length base-32 truncation collision regime (v1.0.5) */ inline constexpr size_t SID_MAX_LEN = 16; constexpr size_t bid_entropy_needed(size_t len) { return (len * 5 + 7) / 8; } std::optional base32_encode(const uint8_t* data, size_t len); std::optional bid_generate(size_t len, const uint8_t* entropy, size_t entropy_len); std::optional bid_generate(size_t len); constexpr size_t sid_entropy_needed() { return 8; } std::optional sid_generate( std::string_view oid, std::string_view did, std::string_view iid, uint32_t counter, size_t len, const uint8_t* entropy = nullptr, size_t entropy_len = 0); } // namespace id /* ══════════════════════════════════════════════════════════════════ * SidPool — Session ID generator with length optimization (§11.2) * * Every session gets a fresh SID. SIDs are never reused. * Length starts at SID_MIN_LEN and grows only on collision. * * acquire_unique() — spec §11.2 conformant: checks each candidate against * the active sessions and grows the length until * unique. THE DEFAULT PATH for any implementation that * claims spec conformance. * acquire_unchecked() — non-conformant escape hatch: fresh counter + entropy, * NO uniqueness check (see §11.2 "Non-conformant * Escape Hatch"). Intended only for constrained-MCU * implementations that cannot maintain an active-SID * set; CALLING THIS FORFEITS THE SPEC §11.2 * UNIQUENESS GUARANTEE. Two SIDs can collide at the * minimum length under concurrent-session counts (the * base-32 truncation birthday bound). * ══════════════════════════════════════════════════════════════════ */ class SidPool { public: using CollisionCheck = std::function; SidPool(std::string_view oid, std::string_view did, std::string_view iid); ~SidPool(); SidPool(const SidPool&) = delete; SidPool& operator=(const SidPool&) = delete; SidPool(SidPool&&) noexcept; SidPool& operator=(SidPool&&) noexcept; std::optional acquire_unique(CollisionCheck check); std::optional acquire_unchecked(); private: struct Impl; std::unique_ptr impl_; }; /* ══════════════════════════════════════════════════════════════════ * Parser — Byte-at-a-time stream parser (v9 §7) * ══════════════════════════════════════════════════════════════════ */ enum class ParseState { WaitSom, WaitSow, WordType, AfterType, RadixFlag, AfterRadix, UnitFlag, WaitSob, WordBody, Tail, Error, }; class Parser { public: using WordCb = std::function; using TailCb = std::function; using MessageCb = std::function; Parser(); ~Parser(); Parser(const Parser&) = delete; Parser& operator=(const Parser&) = delete; Parser(Parser&&) noexcept; Parser& operator=(Parser&&) noexcept; void on_word(WordCb cb); void on_tail(TailCb cb); void on_message(MessageCb cb); ParseState feed(uint8_t byte); ParseState feed(const uint8_t* data, size_t len); void reset(); void set_tail_length(size_t len); ParseState state() const; size_t total_words() const; size_t total_messages() const; size_t parse_errors() const; private: struct Impl; std::unique_ptr impl_; }; /* ══════════════════════════════════════════════════════════════════ * Verb Builders — stateless frame construction * (v9 spec §9–§11) * ══════════════════════════════════════════════════════════════════ */ namespace bus { std::optional establish(std::string_view bid); std::optional conflict(std::string_view bid); std::optional broadcast(std::string_view text); std::optional broadcast_path(std::string_view text, std::string_view path); std::optional ping(std::string_view bid); std::optional ping_all(); std::optional relay(std::string_view text, uint32_t index, std::string_view path); std::optional discover(std::string_view bid); std::optional discover_response(std::string_view target, std::string_view via); std::optional verify(std::string_view bid); std::optional verify_response( std::string_view oid, std::string_view did, std::string_view iid); std::optional scaleback( std::string_view exclusions, uint16_t head_max, uint16_t tail_max); std::optional acknowledge(std::string_view bid); std::optional exception(std::string_view reason); /* Level 2: Auth (Z-verb) — Ed25519 challenge-response */ std::optional auth_challenge(std::string_view target_bid, std::string_view nonce_hex); std::optional auth_response(std::string_view target_bid, std::string_view key_id, std::string_view sig_hex); /* Level 2: Relay + Auth — Z-verb wrapped in Relay for multi-hop */ std::optional relay_auth_challenge(std::string_view target_bid, std::string_view nonce_hex, uint32_t index, std::string_view path); std::optional relay_auth_response(std::string_view target_bid, std::string_view key_id, std::string_view sig_hex, uint32_t index, std::string_view path); } // namespace bus namespace service { std::optional query(std::string_view capability); std::optional offer(std::string_view bid, std::string_view description); std::optional accept(std::string_view bid, std::string_view sender_bid = {}); } // namespace service namespace session { std::optional call(std::string_view sid, std::string_view target_bid, uint32_t mid, std::string_view payload); std::optional call_blob(std::string_view sid, std::string_view target_bid, uint32_t mid, std::string_view payload, const uint8_t* blob, size_t blob_len); std::optional call_wrap(std::string_view sid, std::string_view target_bid); std::optional status(std::string_view sid, std::string_view target_bid, uint32_t mid); std::optional status_response(std::string_view sid, std::string_view target_bid, uint32_t mid, std::string_view state); std::optional notify(std::string_view sid, std::string_view target_bid, uint32_t mid, std::string_view event); std::optional locate(std::string_view sid); std::optional locate_response(std::string_view sid, std::string_view bid); std::optional resume(std::string_view sid); std::optional finish(std::string_view sid, std::string_view target_bid = {}); } // namespace session /* ══════════════════════════════════════════════════════════════════ * Context — Stateful codec (v9 §8–§11) * ══════════════════════════════════════════════════════════════════ */ enum class SessionState { Idle, Active, Suspended }; inline constexpr int MAX_SESSIONS = 32; class Context { public: using MessageCb = std::function; using OfferCb = std::function; using EventCb = std::function; using RelayCb = std::function; Context(std::string_view oid, std::string_view did, std::string_view iid, std::string_view bid); ~Context(); Context(const Context&) = delete; Context& operator=(const Context&) = delete; std::string_view bid() const; void on_message(MessageCb cb); void on_offer(OfferCb cb); void on_event(EventCb cb); void on_relay(RelayCb cb); size_t feed(const uint8_t* data, size_t len); /* ANTHEOS-CONTEXT-FEED-SWALLOWS-PARSE-ERRORS — whether the fed bytes * actually parsed. `feed` reports how many bytes it took, which is what a * caller needs to advance its buffer and is NOT the same question; before * these, a consumer feeding a corrupt stream saw success forever and had no * accessor anywhere on this type that would say otherwise. * * blob_size_errors() is separate from parse_errors() because the bytes were * well-formed protocol: it is the declared BLOB SIZE that could not be read * (ANTHEOS-BLOB-RADIX-COVERAGE-GAP), and a frame whose tail was skipped for * that reason is not the same event as a malformed frame. */ ParseState parse_state() const; size_t parse_errors() const; size_t blob_size_errors() const; /* ANTHEOS-SCRATCH-IDS-SILENT-DROP — IDs a message carried beyond the * scratch's capacity. Nonzero means the message dispatched was TRUNCATED: * the surplus was previously discarded with no error, no counter and no * state change, so a five-ID message read as a complete four-ID one. */ size_t dropped_ids() const; size_t total_messages() const; /** BLOB tail from the last received message (empty if none). */ std::pair last_tail() const; // Bus scope std::optional establish(); std::optional broadcast(std::string_view text); std::optional ping(std::string_view bid = {}); std::optional exception(std::string_view reason); std::optional verify(std::string_view bid); std::optional verify_response(); std::optional discover(std::string_view bid); std::optional acknowledge(std::string_view bid); // Service scope std::optional query(std::string_view capability); std::optional offer(std::string_view description); std::optional accept(std::string_view bid, std::string_view sender_bid = {}); // Auth scope (Level 2) std::optional auth_challenge(std::string_view target_bid, std::string_view nonce_hex); std::optional auth_response(std::string_view target_bid, std::string_view key_id, std::string_view sig_hex); // Relay + Auth (Level 2: multi-hop Z-verb) std::optional relay_auth_challenge(std::string_view target_bid, std::string_view nonce_hex, uint32_t index, std::string_view path); std::optional relay_auth_response(std::string_view target_bid, std::string_view key_id, std::string_view sig_hex, uint32_t index, std::string_view path); // Session scope int session_open(); int session_accept(std::string_view sid, uint32_t inbound_mid); std::string_view session_sid(int slot) const; uint32_t session_mid(int slot) const; SessionState session_state(int slot) const; std::optional session_call(int slot, std::string_view target_bid, std::string_view payload); std::optional session_call_blob(int slot, std::string_view target_bid, std::string_view payload, const uint8_t* blob, size_t blob_len); std::optional session_notify(int slot, std::string_view target_bid, std::string_view event); std::optional session_status(int slot, std::string_view target_bid); std::optional session_close(int slot, std::string_view target_bid = {}); private: struct Impl; std::unique_ptr impl_; }; /* ══════════════════════════════════════════════════════════════════ * LOGICAL body grammar (v9 spec §6.2.1) * ══════════════════════════════════════════════════════════════════ */ namespace logic { /* The parsed shape of a LOGICAL body. SYNTAX ONLY — §6.2.1 makes atom * semantics consumer-defined, so this says which atoms appear and how they * combine and nothing about what they mean. Scaleback reads them as flag * exclusions; another consumer may read them as literals; the grammar holds * either way. * * `left` is the operand of a Not and the left operand of a binary node; `atom` * is set only for Kind::Atom. Move-only, because the children are owned. */ struct LogicalExpr { enum class Kind { Atom, Not, And, Or }; Kind kind = Kind::Atom; char atom = 0; std::unique_ptr left; std::unique_ptr right; }; /* Parse a §6.2.1 V1 body. nullopt on any rejected form; `error`, when given, * receives a message naming the cause — the spec asks for a teaching error on * an empty body, which is the rejection a caller reaches by accident, since an * absent body and an empty one are the same bytes on the wire. */ std::optional parse_logical(std::string_view body, std::string* error = nullptr); /* Consumer-supplied atom semantics: given an atom character, is it true? */ using AtomEvaluator = std::function; /* Evaluate against consumer semantics. `&` and `|` short-circuit, so an * evaluator is not called for an atom that cannot change the answer. */ bool evaluate_logical(const LogicalExpr& expr, const AtomEvaluator& atom_eval); /* The canonical body for an AST: parens exactly where dropping them would * change what the string parses back to, and nowhere else. */ std::string emit_logical(const LogicalExpr& expr); } // namespace logic /* ══════════════════════════════════════════════════════════════════ * TIMESTAMP (§6.2.2) + DURATION (§6.2.3) canonical forms * ══════════════════════════════════════════════════════════════════ */ namespace iso { /* An instant, stored canonical: UTC, microsecond precision. A parsed offset is * already converted, so there is no zone field to carry — §6.2.2's emit form is * UTC only and every stored value is directly emittable. */ struct Timestamp { int year = 0, month = 1, day = 1; int hour = 0, minute = 0, second = 0; uint32_t micros = 0; }; /* Parse a §6.2.2 body: `YYYY-MM-DDTHH:MM:SS[.f…][Z|±HH:MM]`. Sub-microsecond * digits are TRUNCATED, never rounded — the spec's own reason is that rounding * admits multiple valid implementations and truncation has one. Rejects named * zones, extended years, leap seconds and an empty body, each with a message in * `error` when given. */ std::optional parse_timestamp(std::string_view body, std::string* error = nullptr); /* The §6.2.2 canonical form: `YYYY-MM-DDTHH:MM:SS.uuuuuuZ`, 27 bytes exactly. */ std::string emit_timestamp(const Timestamp& ts); /* An interval. `weeks_form` records that the body said `PnW`, which §6.2.3 * keeps rather than converting to days: the component shape is part of what was * said, and the canonical form preserves it. */ struct Duration { bool negative = false; bool weeks_form = false; long long weeks = 0; long long years = 0, months = 0, days = 0; long long hours = 0, minutes = 0, seconds = 0; uint32_t micros = 0; }; /* Parse a §6.2.3 body: `[-]P[nY][nM][nD][T[nH][nM][nS]]` or `[-]PnW`. The two * forms are mutually exclusive. Rejects a bare `P`, mixed weeks+calendar, a * missing `P` prefix and time components without their `T`. */ std::optional parse_duration(std::string_view body, std::string* error = nullptr); /* The §6.2.3 canonical form: minimal components, zero as `PT0S`, weeks form * preserved, negative direction preserved, seconds fractional only when there * is a fraction and then to six digits. */ std::string emit_duration(const Duration& du); } // namespace iso } // namespace antheos