Skip to content

fix(cf-mcp): add invite_code to campfire_join schema + fix hosted store routing#19

Closed
baron-3dl wants to merge 11 commits intomainfrom
work/campfire-agent-a27
Closed

fix(cf-mcp): add invite_code to campfire_join schema + fix hosted store routing#19
baron-3dl wants to merge 11 commits intomainfrom
work/campfire-agent-a27

Conversation

@baron-3dl
Copy link
Copy Markdown
Contributor

Summary

  • campfire_join MCP tool schema was missing invite_code parameter — MCP clients couldn't know to send it, making the invite code security system unreachable via MCP
  • Also fixes a hosted-mode bypass: invite validation was queried against the joining session's local store, which has no invite records for campfires created by other sessions; now routes to the campfire owner's store via transportRouter
  • Schema test TestInvite_JoinSchemaHasInviteCode was already present in HEAD (prior swarm agent); this PR delivers the actual schema fix it was testing

Test plan

  • go test ./cmd/cf-mcp/... -count=1 — all tests pass (41s)
  • TestInvite_JoinSchemaHasInviteCode passes — schema declares invite_code as optional property
  • TestInvite_JoinWithValidCode passes — end-to-end invite flow works
  • TestInvite_JoinWithoutCodeFails passes — enforcement still blocks codeless joins when codes exist

Closes campfire-agent-a27.

🤖 Generated with Claude Code

baron-3dl and others added 11 commits March 24, 2026 14:18
…daries

Replace aspirational security language with a per-deployment-mode breakdown
matching design-mcp-security.md §2. Key corrections:
- Identity sovereignty qualified: server holds your key in hosted mode
- Key wrapping described as encryption at rest, not operator trust reduction
- Blind relay benefit scoped to mixed-mode campfires only
- Security properties table added per deployment mode
- Non-goals section: operator impersonation impossible to prevent in all-hosted;
  confidentiality zero for all-hosted even with encryption

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Token-path separation: Session directories now use a stable internal UUID
(internalID) as the filesystem name, not the bearer token. Token never
appears in error messages, log paths, or filesystem paths.

Token issuance registry: getOrCreate now validates tokens against an
in-memory TokenRegistry. Arbitrary unregistered tokens are rejected with
a typed error. campfire_init calls registry.issue() to mint tokens.

Token revocation: New campfire_revoke_session tool invalidates the current
token immediately and closes the session. Subsequent requests with the
revoked token receive HTTP 401.

Token rotation: New campfire_rotate_token tool issues a new token mapped
to the same internalID (session state preserved). Old token enters a 30s
grace period for in-flight requests, then is deleted.

Token expiry: Registry lookup enforces a 1-hour TTL. Expired tokens return
HTTP 401 with a structured 'expired' error message.

Auth middleware: handleMCPSessioned now has a dispatch point for Bearer
and Signed auth schemes. Signed scheme returns 401 'not yet supported'
as the P1 preparation point.

All existing tests updated to use registry.issue()/issueToken() instead
of raw generateToken(), and sessions.Load(internalID) replaced with
getSession(token) helper.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…dates it

Implements tier-1 invite codes per design-mcp-security.md §5.a.

- InviteStore interface (pkg/store/interfaces.go): CreateInvite,
  ValidateInvite, RevokeInvite, ListInvites, LookupInvite,
  HasAnyInvites, IncrementInviteUse
- SQLite implementation (pkg/store/store.go): campfire_invites table
  (campfire_id, invite_code UNIQUE, created_by, created_at, revoked,
  max_uses, use_count, label). Added to UpdateCampfireID rekey table list.
- Azure Table Storage implementation (pkg/store/aztable/aztable.go):
  CampfireInvites table (PK=invite_code, RK=campfire_id)
- handleCreate + handleCreateHTTP: auto-generate UUID invite code on
  campfire creation, store it, return invite_code in response
- handleJoin: require invite_code when campfire has any registered codes;
  grace period allows join when no codes exist (backward compatibility);
  increments use_count on valid use
- campfire_invite tool: create additional codes with max_uses and label
- campfire_revoke_invite tool: revoke individual codes
- 8 TDD tests covering all paths including grace period and exhausted codes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the store-only HasAnyInvites assertion with a full handler-path
test: srvB (fresh store, no invites for the campfire) dispatches
campfire_join without an invite_code against a campfire created by srvA.
The grace-period branch (HasAnyInvites=false → allow join) is now
exercised end-to-end through srv.dispatch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The InviteStore methods were added to the unified Store interface
in the invite codes implementation. The ratelimit test's fakeStore
needs matching stubs to compile.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implements SHA256-based blind commit so MCP clients can bind the server
to their intended payload before it is signed and relayed.

- handleSend: accepts optional `commitment` and `commitment_nonce` params;
  stores them as signed tags (`commitment:<hex>`, `commitment-nonce:<hex>`)
  so the commitment is covered by the protocol signature.
- handleRead: detects commitment tags and verifies SHA256(payload + nonce)
  == commitment; adds `commitment_verified` (bool) to each message response
  when a commitment is present. Absent for messages with no commitment.
- campfire_commitment tool: server-side helper that generates a random nonce
  and returns {commitment, nonce} for clients that cannot do crypto.
- No breaking changes: messages without commitment work exactly as before.

TDD: 4 tests added in commit_test.go covering verified, tampered, no-commit,
and helper round-trip cases. All pass; full suite green.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… model §5.c)

When the hosted service creates or joins an encrypted campfire (Encrypted=true),
it now writes its own MemberRecord with role=blind-relay instead of the default
full role. This makes the service genuinely unable to receive epoch key deliveries
from campfire:membership-commit messages (per NewMembershipCommitPayload invariant
already enforced in pkg/campfire/encryption.go).

Changes:
- handleCreate: new 'encrypted' bool parameter sets cf.Encrypted and service role
- handleCreate (non-HTTP path): WriteMember + AddMembership use serviceRole/Encrypted
- handleCreateHTTP: accepts serviceRole param, writes blind-relay MemberRecord
- handleJoin: derives serviceRole from state.Encrypted, writes blind-relay MemberRecord
  and stores Encrypted flag in the membership record for downgrade prevention
- campfire_create tool schema: documents 'encrypted' parameter
- blindrelay_test.go: 5 TDD tests verifying the invariants
  - join encrypted → blind-relay role on disk
  - join non-encrypted → non-blind-relay role
  - create encrypted → blind-relay role on disk
  - create non-encrypted → non-blind-relay role
  - ratelimit correctly counts opaque ciphertext envelopes (no content inspection)

Confirmed: NewMembershipCommitPayload already excludes blind-relay members from
Deliveries map — test exists in pkg/campfire/encryption_test.go, no change needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements the audit/transparency log described in design-mcp-security.md §5.e.

- cmd/cf-mcp/audit.go: AuditEntry struct, AuditWriter with async buffered channel,
  simple binary Merkle tree (computeMerkleRoot), per-agent audit campfire lifecycle
  (loadOrCreateAuditCampfire persists ID to cfHome/audit-campfire-id)
- cmd/cf-mcp/main.go: auditWriter field on server struct; AuditWriter.Log() called
  after each successful action in handleSend, handleJoin, handleCreate,
  handleCreateHTTP, handleExport, handleCreateInvite, handleRevokeInvite;
  campfire_init creates audit campfire and returns audit_campfire_id;
  new campfire_audit tool returns total actions, actions_by_type, latest Merkle root
- cmd/cf-mcp/audit_test.go: TDD tests — send creates entry, audit tool returns
  correct counts, Merkle root determinism, request_hash field presence

Closes campfire-agent-zwf

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ate data race

lookup() was reading entry.revoked, entry.gracePeriodUntil, entry.internalID,
and entry.issuedAt AFTER releasing the RWMutex. A concurrent revoke(),
revokeWithGrace(), or delete() call could mutate or remove the entry between
the RUnlock and the field reads — a classic TOCTOU data race.

Fix: copy all needed fields into local variables while still holding the RLock,
then release the lock before evaluating the copies.

Adds TestTokenRegistry_LookupRace to exercise concurrent lookup/revoke/
revokeWithGrace access; -race detection confirms no races with the fix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… store routing

The campfire_join tool schema was missing invite_code as a declared parameter,
making the invite code system unreachable via MCP clients (they consult the
schema to know what to send).

Also fixes a hosted-mode bypass: HasAnyInvites was queried against the joining
session's local store, which never has invite records for campfires created by
other sessions. Now routes invite validation through the transport router to
the campfire owner's store when running in hosted HTTP mode.

Closes campfire-agent-a27.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@baron-3dl
Copy link
Copy Markdown
Contributor Author

Already on main: invite_code wired into campfire_join schema. Closing as obsolete.

@baron-3dl baron-3dl closed this Apr 16, 2026
@baron-3dl baron-3dl deleted the work/campfire-agent-a27 branch April 16, 2026 23:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant