fix(cf-mcp): add invite_code to campfire_join schema + fix hosted store routing#19
Closed
fix(cf-mcp): add invite_code to campfire_join schema + fix hosted store routing#19
Conversation
…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>
Contributor
Author
|
Already on main: invite_code wired into campfire_join schema. Closing as obsolete. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
campfire_joinMCP tool schema was missinginvite_codeparameter — MCP clients couldn't know to send it, making the invite code security system unreachable via MCPtransportRouterTestInvite_JoinSchemaHasInviteCodewas already present in HEAD (prior swarm agent); this PR delivers the actual schema fix it was testingTest plan
go test ./cmd/cf-mcp/... -count=1— all tests pass (41s)TestInvite_JoinSchemaHasInviteCodepasses — schema declaresinvite_codeas optional propertyTestInvite_JoinWithValidCodepasses — end-to-end invite flow worksTestInvite_JoinWithoutCodeFailspasses — enforcement still blocks codeless joins when codes existCloses campfire-agent-a27.
🤖 Generated with Claude Code