Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 44 additions & 3 deletions cmd/codeaf/copied_pingpong_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type copiedPingPong struct {
initialRoot string
door string
replies []string
realBuckets bool
}

type copiedTakeResult struct {
Expand Down Expand Up @@ -174,8 +175,11 @@ func (h *copiedPingPong) takeB() (cell.Cell, error) {
}
return h.initialRoot
}
c := h.b.Continuer(h.eb, syncsetup.TakeOptions{DeviceName: "computer B", RootFor: rootFor, OnPhase: h.trace,
BucketFor: func(string) string { return filepath.Join(h.b.Home, "v3", "projects", "copy--project") }})
bucket := func(string) string { return filepath.Join(h.b.Home, "v3", "projects", "copy--project") }
if h.realBuckets {
bucket = takeCopyBucket
}
c := h.b.Continuer(h.eb, syncsetup.TakeOptions{DeviceName: "computer B", RootFor: rootFor, OnPhase: h.trace, BucketFor: bucket})
door := h.door
if h.copyRoot == "" {
door = "home"
Expand All @@ -194,14 +198,51 @@ func (h *copiedPingPong) takeB() (cell.Cell, error) {
func (h *copiedPingPong) takeA() cell.Cell {
h.t.Helper()
h.t.Setenv(home.EnvVar, h.a.Home)
c := h.a.Continuer(h.ea, syncsetup.TakeOptions{DeviceName: "computer A", RootFor: func(string) string { return h.c.Root }, OnPhase: h.trace})
options := syncsetup.TakeOptions{DeviceName: "computer A", RootFor: func(string) string { return h.c.Root }, OnPhase: h.trace}
if h.realBuckets {
options.BucketFor = takeCopyBucket
}
c := h.a.Continuer(h.ea, options)
got, err := h.surfaceTake(h.a, takeover{c}, h.c.Root, "home", "computer B")
if err != nil {
h.t.Fatal(err)
}
return got
}

func TestCopiedChatFromDeepWorkspaceMovesAndReturns(t *testing.T) {
rig := newHostedMoveRig(t)
rig.work = filepath.Join(t.TempDir(), strings.Repeat("nested-project/", 24), "worktree")
if err := os.MkdirAll(rig.work, 0700); err != nil {
t.Fatal(err)
}
rig.ea.Workspace = rig.work
rig.ea.DataRoot = filepath.Join(rig.a.Home, "v3", "stores")
rig.eb.DataRoot = filepath.Join(rig.b.Home, "v3", "stores")
if err := session.SaveMeta(rig.c.Root, session.Meta{ID: rig.c.ID, Workspace: rig.work}); err != nil {
t.Fatal(err)
}
machine, err := preflight.OpenMachine(rig.c.Root, rig.work)
if err != nil {
t.Fatal(err)
}
machine.Observer().Refresh()
h := &copiedPingPong{hostedMoveRig: rig, initialRoot: filepath.Join(rig.b.Home, "v3", "projects", "copies", rig.c.ID), realBuckets: true, door: "home"}
for round := 1; round <= 3; round++ {
fromA := fmt.Sprintf("deep workspace A %d", round)
h.publishTurn(h.a, h.ea, h.c, h.work, "computer A", fromA)
b, err := h.takeB()
if err != nil {
t.Fatalf("B take %d: %v", round, err)
}
h.content(b, filepath.Join(b.Root, "work"), fromA)
fromB := fmt.Sprintf("deep workspace B %d", round)
h.publishTurn(h.b, h.eb, b, filepath.Join(b.Root, "work"), "computer B", fromB)
a := h.takeA()
h.content(a, h.work, fromB)
}
}

func (h *copiedPingPong) content(c cell.Cell, work, text string) {
h.t.Helper()
if body, err := os.ReadFile(filepath.Join(work, "latest.txt")); err != nil || string(body) != text {
Expand Down
9 changes: 8 additions & 1 deletion cmd/codeaf/synctake.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package main

import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"log"
"net/url"
Expand Down Expand Up @@ -333,7 +335,12 @@ func takeRootFor(id string) string {
// takeCopyBucket files each copy under the folder it came from, apart from
// conversations that work in this computer's own folder of the same name.
func takeCopyBucket(origin string) string {
return home.Join("v3", "projects", "copy-"+encodeWorkspace(origin))
name := "copy-" + encodeWorkspace(origin)
if len(name) > 255 {
digest := sha256.Sum256([]byte(filepath.Clean(origin)))
name = "copy-" + hex.EncodeToString(digest[:])
}
return home.Join("v3", "projects", name)
}

// deviceName is what this machine is called in a sentence about it: the name a
Expand Down
26 changes: 26 additions & 0 deletions docs/changes/unreleased/2162-hosted-relay-authority-fastpath.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
kind: fixed
title: Reuse confirmed hosted authority and prepare new identity peers asynchronously
pr: 2162
surface: [remote, chat]
invalidates:
- Hosted reads previously repeated quorum work and synchronous presence updates; they now reuse confirmed authority while failing closed when it expires.
- New empty identities previously waited for three voting replicas; their first change now commits on one voter before background joint-consensus formation.
---

Directory mutations remain consensus commits. Committed revocations close device
watches and held store polls immediately. Authorization is checked once before
blob work and repeated after I/O only when the applied index changed. Advisory
presence is coalesced outside the request path.

Internal quorum reads preserve an in-progress idle transition; ordinary reads
still wake the group. Link approvals validate certificates and grants before
reserving a pairing request. Exact retries of a committed decision remain valid
after its original pending deadline; expired undecided requests remain refused.

Existing identities retain their voting configuration. New singleton births
persist immutable provenance that older relay code cannot serve; upgrades must
preserve compatible recovery rather than restore a local directory writer.

Copied chats use bounded destination names when a source workspace path exceeds
the filesystem component limit, including paths nested by earlier device moves.
15 changes: 10 additions & 5 deletions internal/clusterstate/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ func Sign(c Command, key ed25519.PrivateKey) (Command, error) {
// immutable records from unread records.
func Dependencies(c Command) []RecordKey {
keys := []RecordKey{QuiescenceKey(), IdentityKey(), DeviceKey(c.Actor), AuthorityKey(c.Actor), SessionKey(c.Actor, c.Session), SessionsKey()}
if c.Change.InitMembership != nil {
if c.Change.InitMembership != nil || c.Change.PrepareQuiescence != nil {
keys = append(keys, LegacyImportKey())
}
m := c.Change
Expand Down Expand Up @@ -360,11 +360,13 @@ func evaluateCommand(c Command, meta ApplicationMeta, read ReadRecord, paged boo
// Old imported groups retain their exact manifest and rows. Their FIRST
// registry command may use only a manifest-provisioned anchor key; it logs
// the authority installation instead of rewriting a frozen import snapshot.
if rows[AuthorityKey(c.Actor)] == nil && c.Change.InitMembership != nil {
// Before that installation, the initial singleton anchor may only prepare
// a quorum-confirmed idle marker for its actual one-voter configuration.
if rows[AuthorityKey(c.Actor)] == nil && (c.Change.InitMembership != nil || c.Change.PrepareQuiescence != nil && len(meta.Configuration.Voters) == 1 && !meta.Configuration.Joint()) {
var manifest LegacyManifest
if raw := rows[LegacyImportKey()]; raw != nil && Decode(raw, &manifest) == nil && manifest.Validate() == nil && manifest.Config.Cluster == c.Cluster && bytes.Equal(manifest.Config.SigningKey, identity.SigningKey) {
for i, node := range manifest.Config.Anchors {
if c.Actor == AnchorActor(node) && i < len(manifest.Config.AnchorKeys) {
if c.Actor == AnchorActor(node) && i < len(manifest.Config.AnchorKeys) && (c.Change.InitMembership != nil || i == 0 && meta.Configuration.Voters[0] == node) {
auth = Authority{Cluster: c.Cluster, Key: manifest.Config.AnchorKeys[i], Anchor: true}
}
}
Expand Down Expand Up @@ -419,7 +421,7 @@ func evaluateCommand(c Command, meta ApplicationMeta, read ReadRecord, paged boo
return out, nil
}
}
service := c.Change.RenewHolds != nil || c.Change.RecordUpload != nil || c.Change.AdmitJoin != nil || c.Change.CompleteJoin != nil || auth.Effects && (c.Change.WorkerBeatBatch != nil || c.Change.DeviceSeen != nil)
service := c.Change.RenewHolds != nil || c.Change.RecordUpload != nil || c.Change.AdmitJoin != nil || c.Change.CompleteJoin != nil || auth.Effects && (c.Change.WorkerBeatBatch != nil || c.Change.DeviceSeen != nil || c.Change.AdvanceTime != nil)
if service != auth.Effects || auth.Personal && auth.Effects || auth.Anchor && (auth.Personal || auth.Effects || (c.Change.EnrollMember == nil || c.Change.EnrollMember.Admission == nil) && (c.Change.RemoveMember == nil || c.Change.RemoveMember.Admission == nil) && c.Change.InitMembership == nil && c.Change.PrepareQuiescence == nil && c.Change.SettleMembership == nil && c.Change.SettleLeadership == nil && c.Change.CompleteLeadership == nil && c.Change.RestoreAnchors == nil && c.Change.CloseSession == nil) {
return out, nil
}
Expand Down Expand Up @@ -521,7 +523,10 @@ func evaluateCommand(c Command, meta ApplicationMeta, read ReadRecord, paged boo
}
}
} else if service {
code = applyEffect(c, out.Now, rows, put)
// A clock witness advances logical time without stamping presence.
if c.Change.AdvanceTime == nil {
code = applyEffect(c, out.Now, rows, put)
}
} else {
if assignmentMutation(c.Change) {
code = applyAssignment(c, out.Now, identity, rows, put, &out.Result)
Expand Down
12 changes: 10 additions & 2 deletions internal/clusterstate/effects.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ func applyEffect(c Command, now int64, rows map[RecordKey][]byte, put func(Recor
return OK
case c.Change.AdmitJoin != nil:
v := c.Change.AdmitJoin
if v.Code == "" || len(v.PublicKey) != ed25519.PublicKeySize || v.Expires <= now || v.Expires > int64(MaxSafeInteger) {
if v.Code == "" || len(v.PublicKey) != ed25519.PublicKeySize || v.Expires > int64(MaxSafeInteger) {
return BadRequest
}
k := JoinKey(v.Code)
Expand All @@ -156,11 +156,19 @@ func applyEffect(c Command, now int64, rows map[RecordKey][]byte, put func(Recor
if Decode(raw, &r) != nil {
return BadRequest
}
if bytes.Equal(r.PublicKey, v.PublicKey) && r.Expires == v.Expires && r.Platform == PlatformOf(v.Platform) {
sameAdmission := bytes.Equal(r.PublicKey, v.PublicKey) && r.Expires == v.Expires && r.Platform == PlatformOf(v.Platform)
// Pending expiry cannot invalidate an already committed decision replay.
if v.Expires <= now && (!sameAdmission || r.Decision == nil) {
return BadRequest
}
if sameAdmission {
return OK
}
return Conflict
}
if v.Expires <= now {
return BadRequest
}
put(k, JoinRequest{Device: DeviceIDOfKey(v.PublicKey), PublicKey: v.PublicKey, Expires: v.Expires, Platform: PlatformOf(v.Platform)})
return OK
case c.Change.CompleteJoin != nil:
Expand Down
57 changes: 57 additions & 0 deletions internal/clusterstate/effects_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,63 @@ func TestR16R17R21EffectsAuthorizationCampaign(t *testing.T) {
})
}
}
func TestR16DecidedJoinAdmissionSurvivesPendingExpiry(t *testing.T) {
public := actorKey("joiner").Public().(ed25519.PublicKey)
admission := cs.AdmitJoin{Code: "request", PublicKey: public, Expires: 1002}
decision := cs.JoinDecision{Code: "request", Device: cs.DeviceIDOfKey(public), Cert: joinCert(t, "joiner"), Grant: "c2VhbGVk", Record: cs.Device{V: 1, Name: "c2VhbGVk"}}
t.Run("decided replay preserves the original decision", func(t *testing.T) {
a := newApplication(t)
if r := service(a, 1000, cs.Mutation{AdmitJoin: &admission}); r.Code != cs.OK {
t.Fatal(r)
}
if r := a.run("a", 1001, cs.Mutation{DecideJoin: &decision}); r.Code != cs.OK {
t.Fatal(r)
}
if r := service(a, 1003, cs.Mutation{AdmitJoin: &admission}); r.Code != cs.OK {
t.Fatal("pending expiry blocked an existing decision", r)
}
if r := a.run("a", 1004, cs.Mutation{DecideJoin: &decision}); r.Code != cs.OK {
t.Fatal("the original decision was not idempotent", r)
}
if r := a.run("a", 1005, cs.Mutation{DecideJoin: &cs.JoinDecision{Code: "request", Deny: true}}); r.Code != cs.AlreadyDecided {
t.Fatal("a contradictory decision changed the outcome", r)
}
extended := admission
extended.Expires = 2000
if r := service(a, 1006, cs.Mutation{AdmitJoin: &extended}); r.Code != cs.Conflict {
t.Fatal("re-admission changed a decided request's lifetime", r)
}
mismatched := admission
mismatched.PublicKey = actorKey("different").Public().(ed25519.PublicKey)
if r := service(a, 1007, cs.Mutation{AdmitJoin: &mismatched}); r.Code != cs.BadRequest {
t.Fatal("expired re-admission changed the immutable requester", r)
}
})
t.Run("expired pending admission cannot be revived", func(t *testing.T) {
a := newApplication(t)
if r := service(a, 1000, cs.Mutation{AdmitJoin: &admission}); r.Code != cs.OK {
t.Fatal(r)
}
if r := service(a, 1003, cs.Mutation{AdmitJoin: &admission}); r.Code != cs.BadRequest {
t.Fatal("an expired pending request was admitted again", r)
}
extended := admission
extended.Expires = 2000
if r := service(a, 1004, cs.Mutation{AdmitJoin: &extended}); r.Code != cs.Conflict {
t.Fatal("re-admission extended the pending lifetime", r)
}
if r := a.run("a", 1005, cs.Mutation{DecideJoin: &decision}); r.Code != cs.RequestGone {
t.Fatal("an expired pending request acquired a decision", r)
}
})
t.Run("unseen expired admission is refused", func(t *testing.T) {
a := newApplication(t)
if r := service(a, 1003, cs.Mutation{AdmitJoin: &admission}); r.Code != cs.BadRequest {
t.Fatal("an unseen expired request was admitted", r)
}
})
}

func TestR16PairingCertificateAndStickyRevocation(t *testing.T) {
a := newApplication(t)
pub := actorKey("joiner").Public().(ed25519.PublicKey)
Expand Down
11 changes: 11 additions & 0 deletions internal/manual/chat/coordination-and-workers.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,17 @@ An open Home or compatibility events viewer pings every 10 seconds. The platform

Connections naming held chats share a renewal cycle every 30 seconds on both relays while fresh hold evidence is available. Each cycle uses bounded batches; the 10-second presence pings between cycles do not commit another renewal. Presence alone never renews a chat.

## First connection and hosted read authority

A new, empty identity can save its first change before the hosted relay finishes
preparing its other copies. Machine coordination can briefly ask you to retry
until that preparation finishes. Existing identities keep their voting group.

Ordinary hosted reads reuse recently confirmed authority. Once that confirmation
expires, an unavailable majority makes reads fail rather than use stale device
permissions. Revocation and rotation remain committed changes; device activity
is advisory and can appear after a short delay.

## Hosted relay memory capacity and retry

After every voter has completed the group's idle barrier, the hosted relay frees its working memory for other households and keeps the saved directory in persistent storage. The next request reloads those copies and obtains a fresh majority response. Busy groups keep their memory. When busy groups fill the relay's memory allowance, another activation receives HTTP 503 with retry guidance; retrying after a group sleeps can succeed.
Expand Down
2 changes: 2 additions & 0 deletions internal/raftabi/abi.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ func (r *Runtime) call(op uint32, d *decoder) ([]byte, error) {
return v1Commands(d)
case 41, 42, 43, 44, 45:
return r.pagedApplication(op, d)
case 49:
return r.formation(d)
case 39:
return hostedForward(d)
case 36:
Expand Down
27 changes: 27 additions & 0 deletions internal/raftabi/formation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package raftabi

import (
cs "github.com/Agent-Field/codeaf/internal/clusterstate"
)

func (r *Runtime) formation(d *decoder) ([]byte, error) {
node := r.nodes[d.u32()]
if node == nil {
return nil, errFrame
}
var manifest cs.LegacyManifest
raw, birth, registry := d.blob(), d.blob(), d.blob()
if d.end() != nil || cs.Decode(raw, &manifest) != nil || !validSingleton(manifest, birth) || len(registry) > 0 {
return nil, errFrame
}
plan, err := node.PlanFormation(manifest.Config.Anchors)
if err != nil {
return nil, err
}
e := encoder{}
e.blob(plan.Change)
e.flag(plan.Checkpoint)
e.flag(plan.Settle)
e.flag(plan.Configure)
return e.b, nil
}
2 changes: 1 addition & 1 deletion internal/raftabi/hosted.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func hostedEffect(d *decoder) ([]byte, error) {
if cs.Decode(raw, &change) != nil && (j.Decode(&change) != nil || j.Decode(new(any)) != io.EOF) {
return nil, errFrame
}
if change.DeviceSeen == nil && change.RenewHolds == nil && change.AdmitJoin == nil && change.CompleteJoin == nil && change.RecordUpload == nil {
if change.AdvanceTime == nil && change.DeviceSeen == nil && change.RenewHolds == nil && change.AdmitJoin == nil && change.CompleteJoin == nil && change.RecordUpload == nil {
return nil, errFrame
}
key := ed25519.NewKeyFromSeed(seed)
Expand Down
27 changes: 26 additions & 1 deletion internal/raftabi/legacy.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"github.com/Agent-Field/codeaf/internal/clusterstate"
pb "go.etcd.io/raft/v3/raftpb"
"google.golang.org/protobuf/proto"
"strconv"
)

// The hosted source feeds original SQL JSON to this bounded conversion call.
Expand Down Expand Up @@ -36,6 +37,10 @@ func legacyCodec(d *decoder) ([]byte, error) {
func legacyGenesis(d *decoder) ([]byte, error) {
var m clusterstate.LegacyManifest
raw := d.blob()
var birth []byte
if len(d.b) > 0 {
birth = d.blob()
}
if e := d.end(); e != nil {
return nil, e
}
Expand All @@ -45,7 +50,14 @@ func legacyGenesis(d *decoder) ([]byte, error) {
e := encoder{}
e.message(&pb.Entry{Index: proto.Uint64(1), Term: proto.Uint64(1)})
e.message(&pb.HardState{Term: proto.Uint64(1), Commit: proto.Uint64(1)})
e.message(&pb.ConfState{Voters: m.Config.Anchors})
voters := m.Config.Anchors
if len(birth) > 0 {
if !validSingleton(m, birth) {
return nil, clusterstate.ErrMigration
}
voters = voters[:1]
}
e.message(&pb.ConfState{Voters: voters})
return e.b, nil
}

Expand Down Expand Up @@ -73,3 +85,16 @@ func legacyAuthorities(d *decoder) ([]byte, error) {
}
return out.b, nil
}

func validSingleton(m clusterstate.LegacyManifest, raw []byte) bool {
var birth struct {
Cluster string `json:"cluster"`
Digest string `json:"digest"`
Voters []string `json:"voters"`
}
if clusterstate.Decode(raw, &birth) != nil || m.Validate() != nil || m.Version != 0 || len(m.Config.Anchors) != 3 || birth.Cluster != m.Config.Cluster || birth.Digest != m.Digest || len(birth.Voters) != 1 || birth.Voters[0] != strconv.FormatUint(m.Config.Anchors[0], 10) {
return false
}
rows, err := clusterstate.LegacyAuthorities(m.Config)
return err == nil && m.Rows == uint64(len(rows)+1)
}
Loading
Loading