Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .github/workflows/capture-vectors.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: capture-vectors

# Build the pinned Ableton/link reference and record protocol test vectors in
# isolated network namespaces. The capture script generates an observed-fact
# manifest per vector (tools/analyze_pcap.py) and fails if any capture does
# not structurally contain the events its scenario demonstrates
# (tools/check_vectors.py). Vectors are uploaded as a build artifact; they are
# committed to the repo only by a maintainer running tools/capture-vectors.sh
# locally and reviewing pcaps + manifests together (captures are not
# byte-reproducible — see vectors/README.md).

on:
workflow_dispatch:
push:
paths:
- tools/capture-vectors.sh
- tools/analyze_pcap.py
- tools/check_vectors.py
- .github/workflows/capture-vectors.yml
- LAST_REVIEWED_SHA

permissions:
contents: read

jobs:
capture:
runs-on: ubuntu-latest
steps:
- name: Checkout spec repo
uses: actions/checkout@v4

- name: Install build and capture dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
cmake g++ git tcpdump iproute2 util-linux python3 \
jackd2 libjack-jackd2-dev portaudio19-dev libasound2-dev

- name: Build reference, record vectors, generate manifests, assert structure
env:
# Clone the reference OUTSIDE the workspace so it is never vendored
# or accidentally committed (PROVENANCE.md firewall).
LINK_CAPTURE_WORK: ${{ runner.temp }}/link-capture
LINK_CAPTURE_OUT: ${{ github.workspace }}/vectors
run: |
# netns isolation (unshare --net) and tcpdump need root; the runner
# provides passwordless sudo. Manifest generation and the structural
# assertions run inside the script and fail the job on a hollow
# capture.
sudo --preserve-env=LINK_CAPTURE_WORK,LINK_CAPTURE_OUT \
bash tools/capture-vectors.sh

- name: Upload vectors artifact
uses: actions/upload-artifact@v4
with:
name: link-wire-vectors
path: |
vectors/*.pcap
vectors/manifests/*.md
if-no-files-found: error
46 changes: 46 additions & 0 deletions .github/workflows/conformance-selftest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: conformance-selftest

# Validate the conformance harness end-to-end by running it in self-test mode
# (reference vs reference): builds the pinned reference into the runner's
# temp dir (never the workspace), runs every scenario in an isolated network
# namespace, and fails on any failed observation.

on:
workflow_dispatch:
push:
paths:
- conformance/**
- tools/build-reference.sh
- .github/workflows/conformance-selftest.yml
- LAST_REVIEWED_SHA

permissions:
contents: read

jobs:
selftest:
runs-on: ubuntu-latest
steps:
- name: Checkout spec repo
uses: actions/checkout@v4

- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
cmake g++ git iproute2 util-linux python3 \
jackd2 libjack-jackd2-dev portaudio19-dev libasound2-dev

- name: Run harness self-test (reference vs reference)
env:
LINK_CAPTURE_WORK: ${{ runner.temp }}/link-reference
run: |
sudo --preserve-env=LINK_CAPTURE_WORK \
bash conformance/run-isolated.sh | tee observations.txt

- name: Upload observation log
if: always()
uses: actions/upload-artifact@v4
with:
name: selftest-observations
path: observations.txt
126 changes: 126 additions & 0 deletions .github/workflows/upstream-watch.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
name: upstream-watch

# Weekly comparison of Ableton/link HEAD against the reviewed pin
# (LAST_REVIEWED_SHA). If no new upstream commit touches wire-relevant paths,
# the pin advances automatically. Otherwise a triage issue is opened (or
# updated) listing the new commits and the wire-relevant paths they touch, so
# a dirty-side author can classify each change (no wire impact / behavioral /
# wire-format) per PROVENANCE.md.
#
# Provenance note: the issue body contains only commit SHAs, dates, and file
# paths — no upstream commit messages, code, or comments.

on:
schedule:
- cron: "17 6 * * 1" # weekly, Monday 06:17 UTC
workflow_dispatch:

permissions:
contents: write
issues: write

env:
UPSTREAM: https://github.com/Ableton/link.git
# Paths in the upstream repo considered wire-relevant. Conservative: the
# whole library (protocol logic AND platform socket configuration both
# produce wire-visible behavior). Examples, docs, CI, and build files are
# not wire-relevant by themselves.
RELEVANT_GLOB: "include/ableton/"

jobs:
watch:
runs-on: ubuntu-latest
steps:
- name: Checkout spec repo
uses: actions/checkout@v4

- name: Compare upstream HEAD against pin
id: cmp
run: |
set -euo pipefail
PIN=$(tr -d '[:space:]' < LAST_REVIEWED_SHA)
git clone --bare --filter=blob:none "$UPSTREAM" /tmp/upstream
HEAD=$(git -C /tmp/upstream rev-parse HEAD)
echo "pin=$PIN" >> "$GITHUB_OUTPUT"
echo "head=$HEAD" >> "$GITHUB_OUTPUT"
if [ "$PIN" = "$HEAD" ]; then
echo "status=current" >> "$GITHUB_OUTPUT"
echo "Pin is current ($PIN)." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
if ! git -C /tmp/upstream merge-base --is-ancestor "$PIN" "$HEAD"; then
echo "status=diverged" >> "$GITHUB_OUTPUT"
exit 0
fi
RELEVANT=$(git -C /tmp/upstream diff --name-only "$PIN" "$HEAD" \
| grep "^$RELEVANT_GLOB" || true)
{
echo "commits<<EOF"
git -C /tmp/upstream log --format='%H %as' "$PIN..$HEAD"
echo "EOF"
echo "relevant<<EOF"
echo "$RELEVANT"
echo "EOF"
} >> "$GITHUB_OUTPUT"
if [ -z "$RELEVANT" ]; then
echo "status=advance" >> "$GITHUB_OUTPUT"
else
echo "status=triage" >> "$GITHUB_OUTPUT"
fi

- name: Advance pin (no wire-relevant changes)
if: steps.cmp.outputs.status == 'advance'
run: |
set -euo pipefail
echo "${{ steps.cmp.outputs.head }}" > LAST_REVIEWED_SHA
git config user.name "upstream-watch"
git config user.email "actions@users.noreply.github.com"
git add LAST_REVIEWED_SHA
git commit -m "upstream-watch: advance pin to ${{ steps.cmp.outputs.head }}

No commit in ${{ steps.cmp.outputs.pin }}..${{ steps.cmp.outputs.head }}
touches wire-relevant paths (include/ableton/)."
git push
echo "Pin advanced to ${{ steps.cmp.outputs.head }} (no wire-relevant changes)." \
>> "$GITHUB_STEP_SUMMARY"

- name: Open or update triage issue (wire-relevant changes)
if: steps.cmp.outputs.status == 'triage'
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
TITLE="upstream triage: ${{ steps.cmp.outputs.pin }}..${{ steps.cmp.outputs.head }}"
BODY=$(cat <<'EOF'
Upstream `Ableton/link` has moved past the reviewed pin and the diff
touches wire-relevant paths. Per PROVENANCE.md, classify each commit:
*no wire impact* (advance pin), *behavioral change* (spec errata +
regenerate vectors), or *wire-format change* (chapter revision + spec
version bump). Record verdicts in CHANGELOG.md.

New commits (SHA, author date):
```
COMMITS_PLACEHOLDER
```

Wire-relevant paths touched:
```
RELEVANT_PLACEHOLDER
```
EOF
)
BODY=${BODY/COMMITS_PLACEHOLDER/"${{ steps.cmp.outputs.commits }}"}
BODY=${BODY/RELEVANT_PLACEHOLDER/"${{ steps.cmp.outputs.relevant }}"}
EXISTING=$(gh issue list --state open --search "in:title \"upstream triage:\"" \
--json number --jq '.[0].number' || true)
if [ -n "$EXISTING" ] && [ "$EXISTING" != "null" ]; then
gh issue comment "$EXISTING" --body "$BODY"
else
gh issue create --title "$TITLE" --body "$BODY"
fi

- name: Report diverged history
if: steps.cmp.outputs.status == 'diverged'
run: |
echo "::warning::Upstream history no longer contains the pin (force push or branch change). Manual review required."
echo "Upstream diverged from pin — manual review required." >> "$GITHUB_STEP_SUMMARY"
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
__pycache__/
*.pyc
79 changes: 75 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,80 @@
All notable changes to this specification. Every entry records the upstream
pin (`Ableton/link` commit) the spec describes at that version.

## [Unreleased] — 0.1.0-draft
## [0.1.0] — 2026-06-11

- Initial scaffolding: provenance rules, upstream-watch workflow, version pin.
- Drafts: 00-overview (serialization, transport model), 03-audio
(LinkAudio v1). Chapters 01-discovery and 02-sync are stubs.
First complete release. Spec text plus test vectors; this is the only artifact
the clean-side implementation ([link-wire-rs](https://github.com/structuresound/link-wire-rs))
is permitted to consume.

Upstream pin: `902aef95bf94af49746fdda5369b42cdcfa1e6d2` (2026-05-19).

### Evidence model

Every claim in the spec is tagged with how it is known (Chapter 0 §1.1): **[W]**
wire-observed (pinned to a released capture via auto-generated manifests and
structural assertions), **[B]** behavioral (dirty-side analysis of the reference,
not exercised by the captures), or **[N]** normative (a requirement of this spec).
Observable facts about the vectors — topology, gateways per peer, message-type
counts, datagram shapes — are *generated from the capture bytes* by
`tools/analyze_pcap.py` into `vectors/manifests/`, and each capture must pass the
per-scenario structural assertions in `tools/check_vectors.py` before release.

### Added

- **Chapter 1 (Discovery):** completed from stub — multicast transport
(`224.76.78.75:20808` v4, `ff12::8080` port 20808 v6), `_asdp_v\x01` framing,
Alive/Response/ByeBye message types, peer-state payload entries (`tmln`, `sess`,
`stst`, `mep4`/`mep6`, `aep4`/`aep6`) with the family-switch rule, ttl-based
timeout/pruning, socket-configuration facts with wire-visible consequences
(notably: multicast loopback only on loopback-address gateways), and full byte
layouts.
- **Chapter 2 (Sync):** completed from stub — `_link_v\x01` ping/pong measurement
protocol, `__ht`/`__gt`/`_pgt`/`sess` entries, the ghost-time transform and median
offset filter, the `tmln` timeline model with beat-origin priority, session
election/merge rules (ghost-time-wins with session-id tie-break, including its
behavior under measurement noise), `stst` start/stop propagation, and the
quantum/phase model with the exact inverse phase-encoding equations. Algorithm
rationale cited to F. Goltz, "Ableton Link — A technology to synchronize music
software," LAC 2018.
- **Test vectors** (`vectors/*.pcap`, CC0), each captured in an isolated network
namespace with a generated manifest: `discovery-join-leave`, `sync-tempo-change`,
`sync-start-stop`, `audio-channel-lifecycle` (including request keepalive
repetitions and a mid-stream tempo change), `multi-gateway-discovery`.
- **Tooling** (MIT): `tools/capture-vectors.sh` (netns-isolated scenario rig),
`tools/analyze_pcap.py` (field-level decoder + manifest generator),
`tools/check_vectors.py` (structural assertions), with CI workflows
`capture-vectors.yml` and `upstream-watch.yml`. Reference source is cloned
outside the repo and never vendored.
- **Conformance harness** (`conformance/`, MIT): drives a reference peer and a
candidate (any program speaking `CANDIDATE-CONTRACT.md`) through the
vector scenarios — discovery join/leave, tempo follow, start/stop, beat
phase alignment, audio announce→subscribe→stream→bye — emitting pass/fail
observations as plain text. Contains no protocol logic (assertions are on
observable endpoint behavior only); self-tests reference-vs-reference in CI
(`conformance-selftest.yml`); ships an example workflow for candidate
repositories. Homed in this repo so the dirty-side-authored harness stays
behind the release gate; the clean side consumes it from a release tag
(PROVENANCE.md firewall item 2).

### Open-question verdicts

| # | Chapter | Question | Verdict | Evidence |
|---|---|---|---|---|
| 00-§4.2 | Overview | string length `N` not bound-checked before construction | Bound is required of implementations (`N` > remaining ⇒ parse error). No on-wire string exceeds its region. | [B] reference analysis; [N] requirement; benign case [W] |
| 00-§4.5(7) | Overview | are duplicate payload-container keys ever legitimate? | No. Senders MUST NOT emit duplicates; receivers apply last-one-wins. (Systematic near-exception: the sync pong's verbatim echo, Ch.2 §4.1.) | absence [W]; semantics [B]; rule [N] |
| 03-1 | Audio | does an `_abu` header precede the AudioBuffer structure? | **No** — payload begins bare with the channel id. | [W] asserted over every captured AudioBuffer |
| 03-2 | Audio | do receivers enforce a 1176- vs 1180-byte payload ceiling? | No receive-side ceiling; bounded only by the 1200-byte socket buffer. 24-byte budget is sender-side only. | [B]; not exercised by any vector |
| 03-3 | Audio | exact derivation of the 50-byte non-audio allowance | None — hand-chosen fixed allowance; encoder subtracts the real chunk-list size at runtime. | [B]; resulting 502-byte cap [W] |
| 03-4 | Audio | receiver behavior for names > 256 bytes | Cap is sender-side only; receivers accept longer length-prefixed names. | [B]; not exercised by any vector |
| 03-5 | Audio | handling of unknown nonzero codec values | Reference parses and decodes as PCM i16 (no recheck). Spec recommends rejecting unknown codecs. | [B]; codec-1-only traffic [W]; recommendation [N] |
| 03-6 | Audio | semantics of nonzero `groupId` | Reserved; MUST send 0, MUST ignore nonzero. | send-0 [W]; drop-nonzero [B]; rule [N] |
| 03-7 | Audio | duplicate payload entries legitimate? | Same as 00-§4.5(7): no. | as above |
| 03-8 | Audio | cross-host usability of advertised IPv6 (`aep6`) addresses | **Deferred** — requires `discovery-ipv6.pcap`; the capture environment's kernel has no IPv6 support. The rig emits it automatically where IPv6 exists. | open |

## [0.1.0-draft] — initial scaffolding

- Provenance rules, version pin.
- Drafts: 00-overview (serialization, transport model), 03-audio (LinkAudio v1).
Chapters 01-discovery and 02-sync were stubs.
- Upstream pin: `902aef95bf94af49746fdda5369b42cdcfa1e6d2` (2026-05-19).
10 changes: 7 additions & 3 deletions PROVENANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ in doubt, express the fact as a table, equation, or state machine.
|---|---|---|
| Spec text (`spec/`) | CC-BY-4.0 | facts only, per the rules above |
| Test vectors (`vectors/`) | CC0 | packet captures of reference peers; protocol facts |
| Tooling (workflows, capture scripts) | MIT | |
| Tooling (workflows, capture scripts, conformance harness) | MIT | the harness (`conformance/`) contains no protocol logic: it asserts on observable endpoint behavior only |

Reference binaries are built from upstream in CI for capture and conformance
purposes and are never redistributed from this repository.
Expand All @@ -50,8 +50,12 @@ purposes and are never redistributed from this repository.
Implementations claiming clean-room provenance from this spec may use ONLY:

1. Released versions of this specification and its test vectors.
2. Conformance-harness results phrased as observations (pass/fail,
measured behavior) — never as reference-source diffs.
2. The released conformance harness (`conformance/`) — which the clean side
may execute against its candidate, fetching this repository at a release
tag into CI caches only (never vendoring it) — and the harness's results
phrased as observations (pass/fail, measured behavior) — never as
reference-source diffs. The harness is authored on the dirty side and is
releasable because it contains no protocol implementation logic.
3. Public non-GPL documentation (the Goltz paper, Ableton's public help
pages and FAQ).

Expand Down
18 changes: 13 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,19 @@ source) and implementers (who must not).

| Chapter | Scope | Status |
|---|---|---|
| [spec/00-overview.md](spec/00-overview.md) | Terminology, transport model, common serialization rules | draft |
| [spec/01-discovery.md](spec/01-discovery.md) | Multicast peer discovery, peer state gossip | stub |
| [spec/02-sync.md](spec/02-sync.md) | Timeline, tempo, clock measurement, start/stop sync | stub |
| [spec/03-audio.md](spec/03-audio.md) | LinkAudio v1: channels, sinks/sources, audio buffers, beat-time alignment | draft |
| `vectors/` | Captured packet traces (golden test vectors) | pending |
| [spec/00-overview.md](spec/00-overview.md) | Terminology, transport model, common serialization rules | v0.1.0 |
| [spec/01-discovery.md](spec/01-discovery.md) | Multicast peer discovery, peer state gossip | v0.1.0 |
| [spec/02-sync.md](spec/02-sync.md) | Timeline, tempo, clock measurement, start/stop sync | v0.1.0 |
| [spec/03-audio.md](spec/03-audio.md) | LinkAudio v1: channels, sinks/sources, audio buffers, beat-time alignment | v0.1.0 |
| [vectors/](vectors/) | Captured packet traces (golden test vectors) with auto-generated observed-fact manifests | v0.1.0 |
| [conformance/](conformance/) | Conformance harness: reference-vs-candidate scenarios emitting pass/fail observations; no protocol logic | v0.1.0 |

Every claim in the spec carries an evidence class (Chapter 0 §1.1): wire-observed
in a vector, behavioral (reference analysis), or normative. Observable facts about
the vectors are generated from the capture bytes
([tools/analyze_pcap.py](tools/analyze_pcap.py)) and structurally asserted
([tools/check_vectors.py](tools/check_vectors.py)), so descriptions cannot drift
from what the captures contain.

## Versioning and upstream tracking

Expand Down
Loading
Loading