# Tier A Federation Ceremony Roadmap

**Author:** Explore agent (automated analysis)  
**Date:** 2026-04-22  
**Status:** Draft for tomorrow's implementation session

---

## 1. What already works (grounded code citations)

The repository has a solid foundation for federation classroom ceremony:

- **Classroom cohort state machine** (`src/axiom/extensions/builtins/classroom/classroom_federation.py:38-172`): `ClassroomCohort` + `CohortMember` dataclasses with ACTIVE/QUARANTINED/REVOKED state transitions, TTL fields, invite-token storage. Transitions implemented: `create_cohort()`, `add_member()`, `quarantine_member()`, `recover_member()`, `revoke_member()`.

- **Membership proof serialization** (`classroom_federation.py:198-217`): `serialize_membership_proof()` produces signed-claim payloads with classroom_id, coordinator_node, member_node, and status. Signature slot reserved for federation layer.

- **Access control helpers** (`classroom_federation.py:179-190`): `member_has_access()` (ACTIVE check) and `broadcast_recipients()` (returns list of eligible ACTIVE member nodes).

- **Pack distribution state model** (`src/axiom/extensions/builtins/classroom/pack_distribution.py:40-100`): `PackDistribution` tracks per-classroom active version, pinned per-member versions, and pending updates. `init_distribution()` pins members to initial version.

- **Federation identity layer** (`src/axiom/federation/identity.py:59-82`, `144-150`): `NodeIdentity` with Ed25519 keypair support (real crypto via `cryptography` library with hashlib fallback), node_id derivation, public-key fingerprinting for TOFU. `generate_identity()` is production-ready.

- **A2A Agent Card structure** (`src/axiom/federation/agent_card.py:14-61`): `AgentCard` dataclass with A2A v0.3 compliance + Axiom extensions (`axiom_node_id`, `axiom_profile`, `axiom_extensions`). `build_agent_card()` factory from NodeIdentity.

- **Federation invitation token generation** (`src/axiom/extensions/builtins/federation/cli.py:330-355`): `_cmd_invite()` produces a 32-byte URL-safe token with TTL, issued-by, and expiry ISO timestamp. Token format: `{"token": ..., "issued_by": node_id, "expires": ..., "ttl_hours": ...}`.

- **Federation join CLI stub** (`federation/cli.py:59-66`, `271-299`): Parser and handler exist; handler currently prints "handshake not yet implemented" and returns 0. No ceremony logic wired.

- **Federation test harness** (`tests/federation_lifecycle/harness.py` + `test_happy_path_3_node.py`): Docker Compose-backed multi-node federation orchestration. Primitive verbs: `start()`, `exec_json()`, `add_peer()`, `assert_federated()`, `teardown()`. Pattern proven for 3-7 node scenarios.

- **Corpus sync one-shot client** (`src/axiom/federation/corpus_sync.py:39-109`): `CorpusSyncClient` with `get_peer_status()`, `find_available_upgrades()`, `sync_status()`. No background daemon—only one-shot comparison. No delta-sync.

**Classroom CLI verbs (currently registered):** `axi classroom prep init|status|corpus|prompt|assessment|rails|rag|lms|dry-run|wrap|demo|brief|explain|compare|student|objectives|export` (verified in `cli.py:718-1230`). **NO `join` verb**.

---

## 2. What's missing for each Tier A item

### Tier A-1: Install discipline
**Status: RESOLVED** (release v0.10.11 republish fixes dispatcher regression).

### Tier A-2: `axi classroom join <invite>` ceremony

**Missing components:**

- **CLI subcommand registration**: `classroom join` verb not in parser (`cli.py`). Need to add to `sub.add_parser()` at ~line 716.

- **Ceremony flow orchestration**: No module bridges federation join (invite token validation, A2A handshake) to classroom membership. Need new file or function in `classroom_federation.py` or adjacent module to:
  1. Parse invite token (validate TTL, extract issuer node_id).
  2. Construct cohort-membership-request payload (student_id, member_node, invite_token, signed by local key).
  3. POST to coordinator's A2A endpoint (URL or node discovery).
  4. Receive signed membership manifest.
  5. Verify coordinator's signature against trust chain.
  6. Persist membership locally.

- **A2A endpoint on coordinator (Rascal)**: No HTTP handler to accept POST `/classroom/join` or similar. Need to wire in `federation/` or new module to:
  1. Validate invite token (check TTL, issuer still trusted).
  2. Verify student identity signature.
  3. Add student to `ClassroomCohort` via `add_member()`.
  4. Sign + return membership manifest.

- **Existing hooks to reuse**:
  - `add_member(cohort, student_id, member_node, invite_token)` at `classroom_federation.py:96-115` — idempotent, directly usable.
  - `serialize_membership_proof(cohort, student_id)` at `classroom_federation.py:198-217` — produces claim structure; signature slot ready.
  - `NodeIdentity.to_manifest()` at `identity.py:69-77` — returns fields for manifest.

**Failure scenario to design around**: Invite token expired or invalid → clear error to student, suggest re-requesting from instructor.

### Tier A-3: Domain pack pull on federation join

**Missing components:**

- **Trigger integration**: `CorpusSyncClient` exists but not called during join. Need to wire `get_peer_status(coordinator_url)` into join flow to discover available pack versions.

- **Pack download**: `CorpusSyncClient.find_available_upgrades()` only detects version gaps; no download logic. Need to fetch pack from coordinator (either via federation pack server or simple HTTP range-request).

- **Local RAG initialization**: No code path that:
  1. Downloads pack chunks to `~/.axi/cache/classroom/{classroom_id}/`.
  2. Indexes chunks into local VectorDB (assumed to exist per infra; not Tier A scope).
  3. Validates pack integrity (checksums).
  4. Signals RAG ready to chat.

- **Existing hooks to reuse**:
  - `CorpusSyncClient.get_peer_status(node_id, url)` at `corpus_sync.py:42-61` — can discover pack metadata.
  - `PackDistribution.init_distribution(cohort, initial_version)` at `pack_distribution.py:56-70` — pins student to version on join.

### Tier A-4: Q&A with citations end-to-end

**Missing components:**

- **RAG policy enforcement**: `src/axiom/extensions/builtins/classroom/rag_policy.py` likely exists (not fully reviewed); confirm it applies classroom + domain pack filtering.

- **Citation metadata plumbing**: No confirmation that Q&A responses include source references (chunk ID, offset, pack version). Likely covered by existing RAG stack but not verified against federation scenario.

- **Tier A requirement**: Assume existing chat + RAG stack works; only integrate with classroom-federated corpus. Accept that instructor + student must be on same domain pack version (no cross-version Q&A yet—Tier B work).

### Tier A-5: Instructor dashboard (TUI minimum viable)

**Missing components:**

- **Cohort status endpoint**: No HTTP endpoint on coordinator that returns cohort state (member list, statuses, joined_at). Need to add to federation/classroom layer.

- **Dashboard CLI or TUI**: No `axi classroom status --cohort` view showing:
  - Total members / active / quarantined / revoked.
  - Per-student node_id, invite accepted timestamp, pack version.
  - Activity feed (member joined, activity timestamp, last Q&A).

- **Activity logging**: No plumbing from student node back to instructor (assumed Tier B: federation daemon 60s heartbeat + activity feed). For Tier A, accept instructor must poll cohort endpoint manually.

- **Existing hooks to reuse**:
  - `ClassroomCohort` dataclass + membership functions already serialize to dict/JSON.
  - Federation daemon scaffold exists (`federation/daemon.py`); could extend with classroom activity sync.

### Tier A-6: Dispatcher regression
**Status: RESOLVED** (0.10.11 republish).

---

## 3. TDD ordering — write tests in this sequence

**Test infrastructure:** Use `FederationHarness` (3+ Docker containers, isolated compose network). Each test in `tests/federation_lifecycle/test_classroom_*.py` following the pattern of `test_happy_path_3_node.py`.

### Test 1: **Invite token structure + validation** (1-2 hours)
- **Test name**: `test_classroom_invite_token_format_and_ttl`
- **Asserts**: 
  - Token is 32-byte URL-safe string.
  - Expires field is ISO-8601 timestamp.
  - Expired token rejected by join handler.
  - TTL argument controls expiry delta.
- **Forces implementation of**:
  - `InviteToken` dataclass or dict structure (token, issued_by, expires, classroom_id).
  - Validation function `validate_invite_token(token, now=None)`.
- **Effort**: 1-2 hours.

### Test 2: **Membership request signing + serialization** (2-3 hours)
- **Test name**: `test_classroom_join_request_signature`
- **Asserts**:
  - Student node can construct a signed join-request payload.
  - Join-request includes (student_id, member_node, invite_token, signature).
  - Coordinator can verify signature using student's public key.
- **Forces implementation of**:
  - `ClassroomJoinRequest` dataclass.
  - `sign_join_request(identity, classroom_id, invite_token)` on student.
  - `verify_join_request(request, student_pubkey)` on coordinator.
- **Effort**: 2-3 hours.

### Test 3: **A2A endpoint: POST /classroom/join** (3-4 hours)
- **Test name**: `test_classroom_ceremony_student_to_coordinator`
- **Asserts**:
  - Student node POSTs signed join-request to coordinator (mocked HTTP or real docker-network A2A port).
  - Coordinator validates invite, signature, and adds student to cohort.
  - Coordinator returns signed membership manifest.
  - Manifest includes classroom_id, student_id, coordinator_node, joined_at, signature.
- **Forces implementation of**:
  - `ClassroomJoinHandler` HTTP endpoint (POST /classroom/join).
  - Coordinator-side cohort registry (persists `ClassroomCohort` + `PackDistribution` state).
  - `serialize_membership_proof()` signature fill-in (federation layer signs).
- **Effort**: 3-4 hours.

### Test 4: **Student node persists membership + verifies coordinator signature** (2-3 hours)
- **Test name**: `test_classroom_join_membership_persisted_locally`
- **Asserts**:
  - Student node receives manifest, verifies signature.
  - Signature verification fails if coordinator key changes (TOFU).
  - Membership written to `~/.axi/classrooms/{classroom_id}/membership.json`.
  - Subsequent `axi classroom status` shows student is joined.
- **Forces implementation of**:
  - Signature verification against coordinator's federated identity.
  - Local persistence of membership proof.
  - Query function for "am I a member of this classroom?"
- **Effort**: 2-3 hours.

### Test 5: **Pack pull on join** (3-4 hours)
- **Test name**: `test_classroom_join_fetches_domain_pack`
- **Asserts**:
  - Join ceremony includes discovery of coordinator's available packs.
  - Student downloads pack (or pack metadata + chunks).
  - Pack is written to cache and indexed into local RAG.
  - `CorpusSyncClient.get_peer_status()` called and results used.
- **Forces implementation of**:
  - Pack download function (HTTP GET with range requests or streaming).
  - Cache directory structure + validation.
  - Integration point: call corpus sync during join.
- **Effort**: 3-4 hours.

### Test 6: **Q&A against federated pack** (2-3 hours)
- **Test name**: `test_classroom_qa_with_citations_on_domain_pack`
- **Asserts**:
  - Student asks question via chat.
  - RAG retrieves from local domain pack (verified to be coordinator's pack).
  - Response includes citations (source chunk ID, pack version).
  - Pack version in response matches membership pinned version.
- **Forces implementation of**:
  - RAG policy filter: only return results from classroom domain pack.
  - Citation metadata attachment.
- **Effort**: 2-3 hours.

### Test 7: **Instructor dashboard cohort view** (2-3 hours)
- **Test name**: `test_classroom_instructor_dashboard_cohort_status`
- **Asserts**:
  - Instructor node can fetch cohort state (3+ students).
  - Cohort status shows member list (student_id, node_id, status, joined_at, activity_timestamp).
  - CLI verb `axi classroom dashboard --cohort {classroom_id}` outputs human-readable table.
- **Forces implementation of**:
  - HTTP endpoint: GET /classroom/{classroom_id}/cohort (protected, instructor-only).
  - Activity logging on student join + Q&A.
  - Dashboard rendering function.
- **Effort**: 2-3 hours.

### Test 8: **Happy path: 12-student federation classroom (end-to-end)** (4-6 hours)
- **Test name**: `test_classroom_federation_12_student_e2e`
- **Asserts**:
  - Instructor creates classroom on Rascal (coordinator).
  - Instructor generates 12 invite tokens.
  - 12 simulated student nodes join via ceremony.
  - All 12 fetch domain pack.
  - Dashboard shows 12 ACTIVE members.
  - 3 students ask questions; responses include citations.
  - Instructor archives classroom cleanly (all members revoked).
- **Forces integration** of all previous tests.
- **Effort**: 4-6 hours (mostly waiting for docker-compose networking + student simulation).

**Total TDD investment: ~22-31 hours over 3-4 days.**

---

## 4. Risk register — verification steps

| Risk | Verification (one-liner) | Mitigation |
|------|--------------------------|-----------|
| **A2A protocol not wired**: Does Rascal expose HTTP endpoints for federation handshake? | `curl -s http://rascal:9877/.well-known/agent-card.json \| jq` — should return AgentCard. | If missing: wire HTTP layer (Flask/FastAPI) in federation daemon. ADR-018 mentions Rascal setup; check if HTTP plumbing is in place. |
| **Cryptography dependency missing**: `from cryptography.hazmat...` fails on student laptop. | `python -c "from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey; print('ok')"` — must not raise. | Dependency already in `pyproject.toml` (assumed); if missing, add and bump version pin. Fallback stub in place (`identity.py:36-50`) but TDD tests must use real crypto. |
| **Rascal federation code doesn't compile**: New federation code has syntax errors on Rascal's Python version. | `docker build -t axiom-test . && docker run axiom-test pytest tests/federation_lifecycle/ -k classroom -v` on Rascal image. | Run locally against Rascal Dockerfile before committing. Rascal may pin older Python; check `adr-018` for version. |
| **Network isolation in compose**: Student nodes can't reach Rascal on internal docker-compose network. | In harness test, `docker compose exec student curl -s http://rascal:9877/health` — must succeed. | docker-compose.yml must define `services: {rascal: {ports: [9877:9877]}, student: {depends_on: [rascal]}}`. Check `tests/federation_lifecycle/docker-compose.yml`. |
| **Clock skew**: Student node system clock > 5 min ahead of Rascal; invite TTL validation fails. | In harness, run `docker exec rascal date && docker exec student date` — must differ < 30s. | Add NTP sync to docker-compose sshd service or accept 60s TTL window for test env. |
| **Invite token reuse**: Same token accepted twice (no revocation tracking). | Generate token T, student1 joins, revoke T, student2 tries → should fail. | Coordinator must track issued + consumed tokens. Add `consumed_at` field to token record. |
| **Membership proof signature validation gap**: Student joins, but chat queries before membership signature is verified. | Test must verify `verify_membership_proof()` is called before allowing RAG access. | Add assertion in chat query handler: `if not classroom.verify_student(ctx.user), raise Unauthorized`. |

---

## 5. Recommended first PR

### PR: **Add `axi classroom join <invite>` CLI + invite token validation**

**Scope** (< 4 hours):
1. Register `classroom join` subcommand in `cli.py` (~30 lines).
2. Implement `_cmd_join()` handler that:
   - Parses invite token from CLI arg.
   - Validates token TTL (not expired).
   - Extracts coordinator node_id + classroom_id from token.
   - Prints "→ joining classroom {classroom_id} on {coordinator_node_id}".
   - Returns 0 (success) with JSON output (`{"joined": true, "classroom_id": ...}`).
3. Add unit test `test_classroom_join_invalid_token_ttl` (validates expired token rejected).
4. Add integration test `test_classroom_join_cli_calls_ceremony` (mocked coordinator response).

**Visible behavior**:
```bash
$ axi classroom join "gAAAAAB..." --confirm
→ joining classroom ab1234... on rascal_n...
  Handshake in progress...
  Membership verified.
  Domain pack syncing...
```

**Real bugs it will surface**:
1. Token format ambiguity (is it base64, URL-safe, or URI?).
2. Invite structure gap (classroom_id not in token yet; need to encode).
3. Missing token storage on coordinator (no way to validate student joins).

**Testable end-to-end**: Yes. Docker 3-node harness (coordinator + student1 + student2). Instructor generates invite. Student1 joins. Student2 gets rejected (token reuse, if implemented). 2 passing tests.

**Deliverable**: Merged PR, green CI, student can run `axi classroom join <invite> --confirm` without error (but noop until ceremony wiring in Tier A-2).

---

## 6. What NOT to do tomorrow

1. **Don't build quarantine/recovery CLI yet.** Data model is solid (`quarantine_member()`, `recover_member()` at `classroom_federation.py:123-158`); the ceremony is the user-visible bit. Quarantine ops are instructor-only Tier B. Focus on happy path (ACTIVE only).

2. **Don't implement Canvas LMS integration.** Classroom enrollment is mocked for Tier A. Real LMS sync (Canvas API) is Tier B. The join ceremony works with manually-issued invites.

3. **Don't build peer-to-peer pack pulls or hierarchical federation.** Tier A assumes Rascal is the single coordinator. Multi-root bridging, Tier B. Simplify.

4. **Don't optimize corpus sync cadence.** `CorpusSyncClient` one-shot is fine for Tier A. No background daemon polling (60s cadence, delta-sync) until Tier B. Call it on join only.

5. **Don't design instructor brief (CHALKE).** Tier A dashboard is TUI table of cohort status. Interactive coaching loop, Tier B. Mention it in the roadmap but don't code it.

---

## Summary

**Tier A federation ceremony is a bounded, testable 7-module stack:**

1. Invite token generation (done) + validation (Test 1).
2. Join request signing (Test 2) → A2A endpoint (Test 3).
3. Membership persistence + signature verification (Test 4).
4. Pack discovery & download (Test 5).
5. RAG integration + citations (Test 6).
6. Instructor dashboard (Test 7).
7. 12-student end-to-end (Test 8).

**Start with:** CLI registration + token validation (4h), then A2A endpoint + ceremony flow (6-8h), then pack pull (4h), then dashboard (3h). 

**Total critical path: ~16-18 hours over 2-3 days.** Integration testing with 12-student harness will surface real bugs in identity binding, network isolation, and signature verification.

**Key lock-in decision:** Invite token format. Once that's frozen, all downstream code follows. Recommend: `{"token": base64, "classroom_id": uuid, "coordinator_id": node_id, "expires": iso8601}`.

