Upgrading to cf 0.30

cf 0.30 is a structural release — layered modules, frozen wire format, new trust authority. New projects should start here. Existing consumers porting from 0.19 have a defined migration path per the breaking-changes inventory below.

Consumer port matrix

Five portfolio consumers are pinned at their current versions while their port windows open. Each will migrate to cf 0.30 as a tracked work item; this page reflects current status.

Consumer Pinned at Key changes Port status
rd 0.19.2 5 — imports, WithNoWalkUp, min_operator_level, convention API post-0.30.0 port pending
dontguess 0.17.0 4 — imports, convention API post-0.30.0 port pending
social 0.19.2 3 — imports, convention API post-0.30.0 port pending
the reach 0.13.1 5 — imports, session, WalkUp, convention API post-0.30.0 port pending
freeso 0.13.0 4 — imports, session, convention API post-0.30.0 port pending
Port order follows dependency depth: rd first (least substrate surface), then dontguess, social, the reach, freeso. Each port is a tracked work item; port windows open sequentially after integration tests pass.

BC-1 — Substrate packages moved to cf-protocol/

All substrate packages previously under pkg/ have moved to cf-protocol/internal/ and are re-exported via cf-protocol/protocol/. Code importing substrate packages directly will not compile.

Fix: Replace direct substrate imports with cf-protocol/protocol. The pkg/protocol forwarding shim continues to compile for the common SDK path. Most consumers (rd, dontguess, social, freeso) only use pkg/protocol and pkg/convention — they are not affected by the substrate move.

Migration details

Import path table:

Before (0.19)After (0.30)
pkg/campfiremoved to cf-protocol/internal/campfire; re-exported via cf-protocol/protocol
pkg/messagemoved to cf-protocol/internal/message; re-exported via cf-protocol/protocol
pkg/storemoved to cf-protocol/internal/store; re-exported via cf-protocol/protocol
pkg/transport/fsmoved to cf-protocol/internal/transport/fs
pkg/transport/httpmoved to cf-protocol/internal/transport/http
pkg/thresholdmoved to cf-protocol/internal/threshold
pkg/projectionmoved to cf-protocol/internal/projection
pkg/predicatemoved to cf-protocol/internal/predicate
pkg/cryptomoved to cf-protocol/internal/crypto
pkg/encodingmoved to cf-protocol/internal/encoding
pkg/admissionmoved to cf-protocol/internal/admission
pkg/tagspecmoved to cf-protocol/internal/tagspec; constants re-exported as protocol.CampfireTagPrefix, etc.

pkg/store/aztable remains at its original path (it implements convention.DispatchStore from pkg/convention, an L2 dependency).

Before (0.19):

import (
    "github.com/campfire-net/campfire/pkg/campfire"
    "github.com/campfire-net/campfire/pkg/protocol"
)

// Direct substrate use:
cf := campfire.New("open", nil, 1)

After (0.30):

import (
    "github.com/campfire-net/campfire/cf-protocol/protocol"
)

// Substrate types are re-exported:
cf := protocol.NewCampfire("open", nil, 1)

// For the common case (Init + client methods): pkg/protocol still compiles
// via the forwarding shim. The SDK-level API is unchanged.
import "github.com/campfire-net/campfire/pkg/protocol"
client, _, err := protocol.Init(cfHome)

BC-2 — Convention layer moved to cf-conventions/

Convention packages (pkg/convention, pkg/durability, cf-convention-extensions/connect/) have moved to the cf-conventions/ Go module with strict layer boundaries.

Fix: Update import paths. Convention authors that previously wired ad-hoc tag prefixes can now rely on protocol.DefaultDeniedTagPrefixes to block reserved campfire: and session: tags at the dispatch boundary.

Migration details

Convention module move:

Before (0.19)After (0.30)
pkg/conventioncf-conventions/cf-convention (interfaces & dispatcher)
pkg/durabilitycf-conventions/cf-durability
cf-convention-extensions/connect/cf-conventions/cf-connect
tag constants in pkg/tagspecre-exported as protocol.CampfireTagPrefix, protocol.CampfireTagMemberJoined, protocol.TagSessionOpen, protocol.TagSessionClose, …

Three new reserved L1 tags are frozen in the wire format:

TagEmitted byMeaning
campfire:visibility-changedClient.Admit / Client.EvictCampfire visibility (open / invite-only) transitioned
session:openprotocol.EmitSessionOpenSession campfire opened; capability template attached
session:closeprotocol.EmitSessionCloseSession campfire closed; coordination log eligible for compaction

Convention declarations must not use these tags as operation tags. The L2 dispatcher rejects any operation whose tag is in the campfire: or session: prefix at parse time with a denied tag prefix error.

Before (0.19):

import "github.com/campfire-net/campfire/pkg/tagspec"

if strings.HasPrefix(msg.Tag, tagspec.CampfirePrefix) { ... }

After (0.30):

import "github.com/campfire-net/campfire/cf-protocol/protocol"

if strings.HasPrefix(msg.Tag, protocol.CampfireTagPrefix) { ... }
// or use cf-conventions/cf-convention for L2 re-exports:
//   convention.DefaultDeniedTagPrefixes

BC-3 — Session token format: v1 tokens deprecated

Session token format v1 (old prefix) is deprecated in 0.30. cf session create now emits cfs2_ tokens with real transport config embedded. Decoding a v1 token returns a clear migration error pointing to this guide.

Fix: Re-generate all session tokens. Any stored v1 token must be replaced. The swarm-coordination convention has been ported to cfs2_. Token TTL is unchanged (2 h default, 24 h maximum).

Migration details

Removed APIs (shared-key session form):

Removed in 0.30Replacement
JoinSession(token string)Admission-based join — worker key is materialized into jail dir; orchestrator key never shared
Shared-key NewSession variantclient.NewSession(ttl) — uses creator’s own identity key
cfs1_ token prefixcfs2_ — embeds real transport config (endpoint URL, transport type) instead of a raw private key

Before (0.19) — CLI:

TOKEN=$(cf session create --ttl 2h)
# Returns: cfs1_<base64-encoded-private-key>

cf session $TOKEN claim-item --item_id rd-abc --description "Fix auth"

After (0.30) — CLI:

TOKEN=$(cf session create --ttl 2h)
# Returns: cfs2_<base64-encoded-transport-config>

cf session $TOKEN claim-item --item_id rd-abc --description "Fix auth"
# cfs1_ tokens return a migration error — regenerate with: cf session create

Before (0.19) — Go (shared-key form):

import "github.com/campfire-net/campfire/pkg/protocol"

// Orchestrator creates a shared-key session:
sess, token, err := client.NewSession(2 * time.Hour)
// token is cfs1_... — shares a private key with all workers

// Worker joins using the shared key:
workerClient, workerSess, err := protocol.JoinSession(token, cfHomeDir)

After (0.30) — Go (lazy-mint form):

import (
    "github.com/campfire-net/campfire/pkg/protocol"
    cfsession "github.com/campfire-net/campfire/cf-conventions/cf-session"
)

// Orchestrator creates session campfire (uses own identity key):
sess, sessionID, err := client.NewSession(2 * time.Hour)

// Emit session:open with capability template for lazy-mint issuance:
openMsg, err := protocol.EmitSessionOpen(client, sessionID, cfsession.CapabilityTemplate{
    Convention: "swarm-coordination",
    OpGlob:     "*",
    TTL:        2 * time.Hour,
})

// Issue per-worker grant on identify:
grant, err := cfsession.IssueWorkerGrant(client, sessionID, workerPubKey, capTemplate)

// Materialize worker identity in jail dir (worker never holds orchestrator key):
workerKeyPath, err := cfsession.MaterializeWorkerIdentity(jailDir, workerPubKey, grant)

BC-4 — Gate language: DefaultGateEvaluator now required

Convention dispatchers without an explicit GateEvaluator will use the new DefaultGateEvaluator from cf-authority. Any custom evaluator must satisfy the L2 GateEvaluator interface declared in cf-convention/gate.go.

Security note: GrantPayload.GranterPubKey (CBOR field 5) is now required for trust-anchor validation. Existing grants without this field will be rejected by DefaultGateEvaluator. Re-issue grants after upgrading.

Migration details

Backward compatibility: the L2 dispatcher ships with a stub AllowAllGateEvaluator as the default. 0.19 code that does not wire a gate evaluator continues to compile and run — you only need the production evaluator when you want real trust enforcement.

Before (0.19):

import "github.com/campfire-net/campfire/pkg/convention"

exec := convention.NewExecutor(client)
exec = exec.WithProvenance(checker)  // v1 interface

After (0.30) — minimal (keep stub evaluator):

import "github.com/campfire-net/campfire/pkg/convention"

// Unchanged — AllowAllGateEvaluator is the default.
exec := convention.NewExecutor(client)
exec = exec.WithProvenance(checker)

After (0.30) — production (wire real evaluator):

import (
    "github.com/campfire-net/campfire/pkg/convention"
    "github.com/campfire-net/campfire/cf-conventions/cf-authority/trust"
)

evaluator := trust.NewDefaultGateEvaluator(trustStore)
exec := convention.NewExecutor(client)
exec = exec.WithGateEvaluator(evaluator)
exec = exec.WithProvenanceV2(trust.NewDefaultProvenanceChecker(store))

Frozen interface contract (cf-authority 1.0):

type GateEvaluator interface {
    Evaluate(ctx context.Context, req EvaluateRequest) EvaluateResult
}

type EvaluateResult struct {
    Decision         Decision    // GateAllow | GateDeny | GateUnresolvable
    Reason           DenyReason  // set when GateDeny
    MissingMessageID string      // set when GateUnresolvable
}

Provenance levels (L2-owned, frozen):

ConstantValueMeaning
ProvenanceLevelAnonymous0valid keypair only
ProvenanceLevelClaimed1self-asserted identity
ProvenanceLevelContactable2challenge/response verified
ProvenanceLevelPresent3level 2 within freshness window

GrantPayload CBOR field 5 (security critical, BC-18): GranterPubKey []byte (Ed25519, 32 bytes) is now required at the last hop. Without it, DefaultGateEvaluator returns Deny / DenyReservedOpFloor — closing a CVE-class trust-anchor bypass. Named struct literal callers are unaffected; only positional GrantPayload construction (test fixtures, custom chain-builders) must add the fifth field.

payload := trust.GrantPayload{
    ParentGrantID: parentID,
    ChildPubkey:   childKey,
    Capabilities:  caps,
    Depth:         1,
    GranterPubKey: granterPubKey,  // FIELD 5 — required at last hop
}

BC-5 — Center-finding removed from substrate

The following substrate symbols are removed in 0.30: WalkUpForCenter, WithWalkUp(), WithNoWalkUp(), WalkUpEnabled(), InitResult.WalkUpPath, InitResult.LocalityChanged, InitResult.DelegationIssued. Center-finding ("walk-up locality") moves to the discovery layer.

Fix: Locality resolution moves to the discovery layer (cf-discovery). Replace any WithWalkUp() calls with discovery-layer equivalents. See the cf-discovery spec for the new discovery-based locality model — snippet wire format (§1), producer/consumer requirements (§5–§6), and post-join verification (§11) collectively replace the old walk-up.

Migration details

Removed symbols:

SymbolDisposition
protocol.WithWalkUp()deleted
protocol.WithNoWalkUp()deleted
protocol.WalkUpEnabled()deleted
InitResult.WalkUpPathfield deleted
InitResult.Recenteredfield deleted
InitResult.DelegationIssuedfield deleted
RecenterClaim typedeleted
RecenterCanonicalPayloaddeleted
config key behavior.walk_upsilently ignored — remove from .cf/config.toml

Before (0.19):

import "github.com/campfire-net/campfire/pkg/protocol"

// Suppress walk-up during import:
client, result, err := protocol.Init(cfHomeDir, protocol.WithNoWalkUp())
if result.Recentered {
    log.Printf("recentered to %s via %v", result.WalkUpPath[0], result.WalkUpPath)
}

// Or enable walk-up explicitly:
client, result, err := protocol.Init(cfHomeDir, protocol.WithWalkUp())

After (0.30):

import "github.com/campfire-net/campfire/cf-protocol/protocol"

// Walk-up option is gone — just call Init:
client, _, err := protocol.Init(cfHomeDir)
// result.Recentered, result.WalkUpPath — do not exist; delete references.

.cf/config.toml before:

[behavior]
walk_up = true

.cf/config.toml after:

# Remove the behavior.walk_up key entirely.
# (It is silently ignored in 0.30; clean it up to avoid confusion.)

New discovery-layer API (cf-discovery):

import "github.com/campfire-net/campfire/cf-conventions/cf-discovery"

// Tier 2 — resolve a name with TOFU pinning:
resolver := cfdiscovery.NewResolver(client)
campfireID, err := resolver.Resolve("baron.dontguess")
// ErrInviteOnly — campfire requires admission
// ErrPostJoinVerificationFailed — campfire failed probe-write verification

// Multi-level chain:
chain, err := resolver.ResolveChain("rd.ready.3dl")
// chain contains per-hop snippets with composed freshness windows.
// Multi-hop chains use the MINIMUM freshness window across all hops —
// a hostile intermediate cannot extend apparent freshness.

Source: docs/upgrade-0.19-to-0.30.md — raw markdown with the full 18-item breaking-changes inventory and per-consumer migration plans.