Together · technical page · July 9, 2026 · for people who build and break software

Two archives agreed on a Saturday walk.

Nine hours apart. Neither one opened the other. The Together protocol: transport-agnostic signed envelopes carrying utterance-only bodies between paired archives, HMAC-authenticated under an out-of-band pair secret, replayed into local state that rebuilds byte-identical. 126 tests of this rail's own, green in the full Personal Neutral Archive (PNA) suite at its July 2026 build (current whole-suite counts live on the build log) — including an independent adversarial pass that found 8 real defect classes, all fixed. The plain-words version is for everyone else.

Reading and sending are different methods

A PNA connects to other people through exactly two methods, kept strictly separate by design. The consent gate handles reading: another person, holding an exact-match grant, reads from your archive. Message transport handles sending: it carries a message you composed, an utterance with provenance. Archive contents are never copied into it. Nothing does both.

Grant rail — the gate (pna-mcp)Sharing rail — message transport (this spec)
What movesAccess: another identity reads your archive through the gateA message: a consented utterance with provenance. No archive contents are copied
DirectionInbound. Scoped, exact-match, logged, C0-invisibleOutbound. Composed, signed, logged, per message
Trust objectA grant in consents/policy.yamlA contact card in your roster, established by pairing
ExampleYour caregiver reads your Medications partitionYou invite a friend for a Saturday walk

A walk invitation is not a grant. Nobody's archive opens. Two sovereign archives exchange sentences, and each writes what it agreed to into its own home. The gate needs zero changes for this rail to exist.

The envelope

An envelope is one utterance in flight: JSON, one file, append-only on both sides.

{ // one signed message — env/thread/verb/from/to/sent_utc/body/sealing/auth "env": "env-01J...", // unique id (ULID-style) — replay/idempotency key "thread": "evt-01J...", // the event this belongs to "verb": "PROPOSE", // PROPOSE·ACCEPT·COUNTER·DECLINE·CONFIRM·ADD·WITHDRAW·CANCEL "from": "carmen", // sender contact-id as known to recipient's roster "to": ["marta"], "sent_utc": "2026-07-09T21:14:03Z", "body": { … }, // composed fields only; archive paths and contents stay out "sealing": "transport", // declared protection level — see below "auth": "hmac-sha256:..." // over canonical bytes, keyed by the out-of-band pair secret }

The utterance-only rule (required): body may contain only what the sender composed for this message (activity, slots, title, note, roster). Never archive paths, never archive contents, never auto-attached items. v0.1 refuses attachments entirely; consented excerpts arrive in v0.2 with per-item consent checks at compose time.

Sealing rungs: declarations enforced, the ladder pattern

RungMeansStanding
transportEnvelope is HMAC-authenticated per-pair; confidentiality comes from the transport (a private repo/folder run by an operator the circle trusts)v0.1 builds this
e2ePayload additionally encrypted to recipient (age/X25519); relay sees routing + ciphertext onlydefined; implements when age is present

Invariant: a transport-sealed envelope must refuse to travel through a transport marked untrusted, declared in the envelope, checked at send, enforced in code. Authenticity in v0.1 is HMAC-SHA256 under the pair secret (stdlib, constant-time compare). Pairwise secrets are sufficient because groups are organizer-fanout stars; per-pair asymmetric keys (Ed25519) arrive with v0.2 introductions, where "who vouched whom" needs real signatures.

Verbs and the state machine

PROPOSE ──► recipient screens (roster) ──► considers PRIVATELY ──► ACCEPT | COUNTER | DECLINE ▲ │ └──────────────── organizer re-offers or ────────────────────────────┘ CONFIRM (fixes the slot) │ every participant's PNA writes ITS OWN record (same UTC instant, each in their own zone, .ics for their calendar app) │ ADD (organizer invites another card) · WITHDRAW (a participant exits, event lives) CANCEL (organizer ends it for everyone — tombstone, history kept) DUPLICATE (local: clone thread → new PROPOSE, "same time next week") DONE (local only: it happened — the archive's own record accrues)

Verb set: PROPOSE · ACCEPT · COUNTER · DECLINE · CONFIRM · ADD · WITHDRAW · CANCEL cross the wire; DUPLICATE · DONE are local acts. State machine (per event): proposed → agreed → done | cancelled, with counter loops before agreement and per-participant status invited | accepted | declined | withdrawn. Out-of-order verbs are rejected. Nothing is repaired: a CONFIRM on a declined thread, an ACCEPT after CANCEL, a second CONFIRM — all refused with one log line. Replayed envelopes (same env id) are idempotent no-ops.

The organizer is the serialization point (star topology); groups are organizer-fanout: participants trust the organizer's assertion of the roster, exactly as a dinner-party host is trusted. At ~10-person scale the star topology makes pairwise primitives enough, so no one needs distributed group cryptography (MLS) here.

Time zones

A slot is a UTC instant plus the proposer's IANA zone name. An offset is never stored, so DST cannot fork the agreement. Every rendering shows both clocks; the recipient's side renders from their zone in their roster identity, so nobody does arithmetic. The agreement is the instant: both archives store the same UTC, and each derives its own local display and its own .ics.

1. Carmen (Palo Alto) → Marta (Madrid), paired since spring: PROPOSE walk-and-talk, 45 min, Sat: slot A 9:00 PT/18:00 CET, slot B Sun 8:00 PT/17:00 CET 2. Marta's PNA: sender known → renders both clocks → local check: Sat collides with lunch at her mother's COUNTER: Sat 10:00 PT/19:00 CET 3. Carmen: fine → CONFIRM slot: Sat 17:00Z both archives write evt-… : same instant — her .ics says 10:00 AM PDT, Marta's says 7:00 PM CEST

Privacy invariants (spec §7)

Storage

Together/ ├── roster/<contact-id>.card.yaml # pairing ceremony output; provenance = how paired ├── events/<evt-id>/ │ ├── exchange/<seq>-<env-id>.env.json # the signed thread, verbatim as sent/received — ORIGINALS │ ├── event.yaml # current state — DERIVED, rebuilt by replaying exchange/ │ └── derived/event.ics # calendar artifact — derived, regenerable ├── courier-log.jsonl # append-only: one line per send/receive/quarantine/reject └── quarantine/ # unopened envelopes from unknown senders

The archive doctrine lands here intact: the envelope thread is canonical; event.yaml is an index. Current state is a pure replay of the signed exchange: delete every event.yaml and pna share rebuild reconstructs them byte-identically (tested). The thread is also the provenance of the agreement itself: who proposed, who countered, who confirmed, when. Records default C1; the gate is not involved. Circle members never read Together/. They hold their own copies.

Enforced versus recorded

Same discipline as the builders page: a claim is ENFORCED only when a test refuses the violation today. RECORDED means written down and honored by convention; FUTURE means designed. The build has not started.

ClaimStandingWhere
Pairing gates everything: unknown sender quarantined unopened, logged, never renderedENFORCEDI3 — test A1
Constant-time HMAC-SHA256 auth under the out-of-band pair secret; one-bit tamper refusesENFORCED§4 — stdlib compare, Session 5's lesson inherited
Utterance-only body allowlist: composed fields only, no archive paths/contents, attachments refused outrightENFORCEDI2 — test A3, zero bytes emitted from a conflict check
Organizer authority: CANCEL/CONFIRM/ADD are organizer-only; history preserved on every sideENFORCEDI4 — test A5
Thread-origin binding: a validly-paired non-organizer cannot plant themselves as organizer on a third party's archiveENFORCEDI4 — defect #3 fix, bound evt-ids
One courier-log line per send/receive/quarantine/reject; unwritable log ⇒ operation refusesENFORCEDI6 — test A7
Transport-sealed envelope refuses to travel through a transport marked untrustedENFORCEDI7 — test A10
State machine refuses disorder (CONFIRM-on-declined, ACCEPT-after-CANCEL, second-CONFIRM, replay)ENFORCEDI5 — test A4
Replay rebuilds truth: delete all event.yaml, rebuild reconstructs byte-identical from exchange/ENFORCEDtest A9
ADD shares the present: a third participant gets current agreed state only, zero negotiation historyENFORCEDI2 — test A8
Cross-zone agreement is one UTC instant; each side's .ics parses with its own local clockENFORCEDtest A2
Real age e2e round-trip: the binary is absent here; compose degrades to transportRECORDEDrefusal logic (I7) is age-independent and tested; the encrypt/decrypt path has no live integration test yet
A live network transport (Signal / shared repo relay reachable off one machine)FUTUREv0.1 uses shared-folder transport; Kurt's lane, post-provisioning
Ed25519 introductions and vouching (who-vouched-whom needs real signatures)FUTUREv0.2, spec §2/§10
MLS/relay federation, once circles cross operators the participants don't already trustFUTURElater, spec §3/§10

Verification

test_pna_share.py: 38 green, including spec §9 acceptance A1–A10 by name — pairing-gates-everything, cross-zone-one-instant, utterances-only, state-machine-refuses-disorder, organizer-only-CANCEL-keeps-history, DUPLICATE-lineage, one-log-line-plus-fail-closed, ADD-shares-present-not-past, replay-rebuilds-truth, transport-refuses-untrusted-transport. test_pna_share_adversarial.py: 88 green — all 14 defect-tests now pass, the rest confirm defenses. Full combined PNA suite at that build: 400 tests, OK, exit 0 (test_pna 119 · test_pna_mcp 59 · test_pna_mcp_adversarial 96 [2 intended xfails, pre-existing] · test_pna_share 38 · test_pna_share_adversarial 88); no pre-existing suite regressed. ~/PNA untouched — access-log still 2 lines, no Together/ directory; every test ran against tempfile archives.

Mid-fix, the builder hit an account session limit (during defect #3): it had already closed defects 5, 6, and 8, and left defect 7 partially wired. The main thread finished the remaining five defects inline and verified each one atomically. A fresh agent would have hit the same limit and risked another mid-flight failure. The interruption and the finish are both part of the record. Neither was smoothed over. That's the house culture.

Defects found while building — recorded, because that's the culture

An independent adversary, told not to read the builder's tests, attacked from spec §7 (I1–I10) and §9 (A1–A10) and found 8 real defect classes, written as failing assertions of correct behavior. All 8 were fixed and re-verified.

Run it yourself

# from KISOP-SYSTEM/pna/cli — stdlib only, Python 3.9+, no installs python3 test_pna_share.py # 38 tests — spec §9 acceptance A1–A10, by name python3 test_pna_share_adversarial.py # 88 tests — independent attack pass, 8 defect classes, all closed   # the shape of one exchange, in the rail's own verbs (pna_share.py) — # illustrative, following the verb vocabulary and the one CLI form the spec names directly (pna share rebuild, §6): pna share pair marta # out-of-band ceremony → roster/marta.card.yaml, pair secret set pna share propose marta --activity "walk & talk" --slots 2026-07-11T16:00Z,2026-07-12T15:00Z pna share send # writes exchange/0001-<env>.env.json, signs, one log line pna share inbox # on Marta's side — renders known senders only pna share confirm evt-01J... # fixes the slot; each side derives its own .ics pna share rebuild # delete every event.yaml; replay exchange/ — byte-identical

The two test commands above are the verified ground truth: run them and read the exit code. The CLI walkthrough below them follows the rail's verb vocabulary faithfully and is illustrative. No published CLI reference is quoted.

Attack here

The F4 adversarial pass inherits these targets; good-faith security research is welcome through the main site's Write to us → security door — a safe harbor written into our Terms, §15.

Where it sits

This is the correspondence rail's first lived application; co-purchase (a neighbor-to-neighbor bulk-buying protocol, built the same week) is its second, riding the same utterance-only discipline. Its receipt-share is the rail's first consented excerpt. This spec names the v0.2 attachment path. We have not built it yet (§4). This spec and the co-purchase session's export-rail draft stand side by side ("one rail, two protocols"), and whether they merge into one is Carmen's call, recorded. Neither build waits on it. Working documents, in the repository: pna/SHARING-SPEC-v0.1.md · pna/SHARING-SPEC-v0.1-export-rail.md · pna/PNA-SHARING-BUILD-REPORT.md · pna/PNA-COPURCHASE-BUILD-REPORT.md.

Where this stands

Prototype, built July 9, 2026, on synthetic data only. Two invented archives, paired the way the spec describes; the founder's real archive was never touched (verified: its access log is unchanged, still 2 lines, no Together/ directory).

Not yet: gate (pna-mcp) integration, a live network transport, the real e2e sealing rung, introductions/Ed25519 vouching, MLS federation, mobile surfaces, recurrence rules. Each is specified with a plain status in the spec above.

Open design calls (Carmen's, recorded; work continues while they wait): the Together/ partition name (alternatives Circles/, Compañía/) — joint with co-purchase's own naming sign-off · outbound-consent framing for a future agent-sends-on-your-behalf grant (v0.2, routes through policy.yaml) · first live transport placement, joint with Kurt · ES invitation wording, riding the terms-pack seam · the one-rail-two-protocols merge with co-purchase's export-rail draft.