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.
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
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/campfire | moved to cf-protocol/internal/campfire; re-exported via cf-protocol/protocol |
pkg/message | moved to cf-protocol/internal/message; re-exported via cf-protocol/protocol |
pkg/store | moved to cf-protocol/internal/store; re-exported via cf-protocol/protocol |
pkg/transport/fs | moved to cf-protocol/internal/transport/fs |
pkg/transport/http | moved to cf-protocol/internal/transport/http |
pkg/threshold | moved to cf-protocol/internal/threshold |
pkg/projection | moved to cf-protocol/internal/projection |
pkg/predicate | moved to cf-protocol/internal/predicate |
pkg/crypto | moved to cf-protocol/internal/crypto |
pkg/encoding | moved to cf-protocol/internal/encoding |
pkg/admission | moved to cf-protocol/internal/admission |
pkg/tagspec | moved 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/convention | cf-conventions/cf-convention (interfaces & dispatcher) |
pkg/durability | cf-conventions/cf-durability |
cf-convention-extensions/connect/ | cf-conventions/cf-connect |
tag constants in pkg/tagspec | re-exported as protocol.CampfireTagPrefix, protocol.CampfireTagMemberJoined, protocol.TagSessionOpen, protocol.TagSessionClose, … |
Three new reserved L1 tags are frozen in the wire format:
| Tag | Emitted by | Meaning |
|---|---|---|
campfire:visibility-changed | Client.Admit / Client.Evict | Campfire visibility (open / invite-only) transitioned |
session:open | protocol.EmitSessionOpen | Session campfire opened; capability template attached |
session:close | protocol.EmitSessionClose | Session 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.30 | Replacement |
|---|---|
JoinSession(token string) | Admission-based join — worker key is materialized into jail dir; orchestrator key never shared |
Shared-key NewSession variant | client.NewSession(ttl) — uses creator’s own identity key |
cfs1_ token prefix | cfs2_ — 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):
| Constant | Value | Meaning |
|---|---|---|
ProvenanceLevelAnonymous | 0 | valid keypair only |
ProvenanceLevelClaimed | 1 | self-asserted identity |
ProvenanceLevelContactable | 2 | challenge/response verified |
ProvenanceLevelPresent | 3 | level 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:
| Symbol | Disposition |
|---|---|
protocol.WithWalkUp() | deleted |
protocol.WithNoWalkUp() | deleted |
protocol.WalkUpEnabled() | deleted |
InitResult.WalkUpPath | field deleted |
InitResult.Recentered | field deleted |
InitResult.DelegationIssued | field deleted |
RecenterClaim type | deleted |
RecenterCanonicalPayload | deleted |
config key behavior.walk_up | silently 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.