Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

428 lines
14 KiB
Go

package mobileremote
import (
"context"
"crypto/ed25519"
"crypto/rand"
"errors"
"net/http"
"strings"
"sync"
"testing"
"time"
authbridge "github.com/tutti-os/tutti/packages/auth/bridge-go"
devicelink "github.com/tutti-os/tutti/packages/device-link"
authenticatedlink "github.com/tutti-os/tutti/packages/device-link/authenticated"
"github.com/tutti-os/tutti/packages/device-link/linkmanager"
mobileremotebiz "github.com/tutti-os/tutti/services/tuttid/biz/mobileremote"
)
type remoteHostControlPlane struct {
mu sync.Mutex
attempt DeviceLinkAttempt
identityKey ed25519.PublicKey
registered chan struct{}
updated chan DeviceLinkAttempt
}
func cloneRemoteHostAttempt(attempt DeviceLinkAttempt) DeviceLinkAttempt {
attempt.STUNEndpoints = append([]string{}, attempt.STUNEndpoints...)
if attempt.CallerICE != nil {
callerICE := *attempt.CallerICE
callerICE.Candidates = append([]string{}, attempt.CallerICE.Candidates...)
attempt.CallerICE = &callerICE
}
if attempt.OwnerICE != nil {
ownerICE := *attempt.OwnerICE
ownerICE.Candidates = append([]string{}, attempt.OwnerICE.Candidates...)
attempt.OwnerICE = &ownerICE
}
return attempt
}
type remoteHostDiagnostics struct {
events chan RemoteAttemptEvent
}
func (d *remoteHostDiagnostics) Record(event RemoteAttemptEvent) {
select {
case d.events <- event:
default:
}
}
func (c *remoteHostControlPlane) RegisterDevice(_ context.Context, _ string, input RegisterDeviceInput) (RegisteredDevice, error) {
if !ed25519.Verify(c.identityKey, identityRegistrationProof(input.DeviceID, input.PublicKey), input.Proof) {
return RegisteredDevice{}, errTestInvalidProof
}
select {
case c.registered <- struct{}{}:
default:
}
return RegisteredDevice{UserDeviceID: "desktop-user-device", DeviceID: input.DeviceID}, nil
}
func (*remoteHostControlPlane) CreateChallenge(context.Context, string, string) (CreateChallengeResult, error) {
return CreateChallengeResult{}, nil
}
func (*remoteHostControlPlane) GetChallenge(context.Context, string, string) (mobileremotebiz.PairingChallenge, error) {
return mobileremotebiz.PairingChallenge{}, nil
}
func (*remoteHostControlPlane) ConfirmChallenge(context.Context, string, string, string, []byte) (ConfirmChallengeResult, error) {
return ConfirmChallengeResult{}, nil
}
func (*remoteHostControlPlane) ListPairings(context.Context, string) ([]mobileremotebiz.DevicePairing, error) {
return []mobileremotebiz.DevicePairing{{
PairingID: "pairing-1", ControllerUserDeviceID: "phone-user-device",
TargetUserDeviceID: "desktop-user-device", State: "active",
}}, nil
}
func (*remoteHostControlPlane) RevokePairing(context.Context, string, string) (mobileremotebiz.DevicePairing, error) {
return mobileremotebiz.DevicePairing{}, nil
}
func (c *remoteHostControlPlane) ListDeviceLinkAttempts(
_ context.Context,
_ string,
pairingID string,
deviceID string,
signature []byte,
) ([]DeviceLinkAttempt, error) {
if pairingID != "pairing-1" || deviceID != "desktop-device" ||
!ed25519.Verify(c.identityKey, deviceLinkProof("list", pairingID, "", ""), signature) {
return nil, errTestInvalidProof
}
c.mu.Lock()
defer c.mu.Unlock()
return []DeviceLinkAttempt{cloneRemoteHostAttempt(c.attempt)}, nil
}
func (c *remoteHostControlPlane) UpdateDeviceLinkParticipant(
_ context.Context,
_ string,
pairingID string,
attemptID string,
deviceID string,
input DeviceLinkParticipantInput,
) (DeviceLinkAttempt, error) {
if pairingID != "pairing-1" || attemptID != "attempt-1" || deviceID != "desktop-device" ||
!ed25519.Verify(c.identityKey, deviceLinkProof("update", pairingID, attemptID, input.Fingerprint), input.IdentitySignature) {
return DeviceLinkAttempt{}, errTestInvalidProof
}
c.mu.Lock()
c.attempt.OwnerFingerprint = input.Fingerprint
c.attempt.OwnerProtocolVersion = input.ProtocolVersion
c.attempt.OwnerICE = &DeviceLinkICEParams{
Ufrag: input.ICE.Ufrag, Pwd: input.ICE.Pwd,
Candidates: append([]string(nil), input.ICE.Candidates...),
}
c.attempt.State = "ready"
updated := cloneRemoteHostAttempt(c.attempt)
c.mu.Unlock()
select {
case c.updated <- cloneRemoteHostAttempt(updated):
default:
}
return updated, nil
}
type testProofError string
func (e testProofError) Error() string { return string(e) }
const errTestInvalidProof = testProofError("invalid test proof")
func TestRemoteHostConnectsAuthenticatedLinkAndServesAgentHTTP(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
caller, err := authenticatedlink.NewParticipant(authenticatedlink.ParticipantConfig{IncludeLoopback: true})
if err != nil {
t.Fatal(err)
}
defer caller.Close()
callerDescription, err := caller.LocalDescription(ctx)
if err != nil {
t.Fatal(err)
}
attemptExpiry := time.Now().Add(8 * time.Second)
controlPlane := &remoteHostControlPlane{
identityKey: publicKey, registered: make(chan struct{}, 1), updated: make(chan DeviceLinkAttempt, 8),
attempt: DeviceLinkAttempt{
AttemptID: "attempt-1", PairingID: "pairing-1",
CallerDeviceID: "phone-device", CallerFingerprint: callerDescription.Fingerprint,
CallerProtocolVersion: deviceLinkProtocolVersion,
CallerICE: &DeviceLinkICEParams{
Ufrag: callerDescription.Ufrag, Pwd: callerDescription.Pwd,
Candidates: []string{},
},
OwnerDeviceID: "desktop-device", State: "awaiting_owner",
ExpiresAt: attemptExpiry.UTC().Format(time.RFC3339Nano),
},
}
diagnostics := &remoteHostDiagnostics{events: make(chan RemoteAttemptEvent, 32)}
service := &Service{
Account: &stubAccount{session: &authbridge.Session{
SessionID: "account-session", Cookie: "session=cookie",
}},
Identities: &stubIdentityStore{identity: mobileremotebiz.DeviceIdentity{
DeviceID: "desktop-device", PublicKey: publicKey, PrivateKey: privateKey,
}},
ControlPlane: controlPlane,
Diagnostics: diagnostics,
RemotePollInterval: 10 * time.Millisecond,
includeLoopback: true,
}
service.StartRemoteHost(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet || r.URL.Path != "/v1/workspaces" {
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"workspaces":[{"workspaceId":"workspace-1"}]}`))
}))
defer service.Close()
select {
case <-controlPlane.registered:
case <-ctx.Done():
t.Fatal("desktop was not registered")
}
var updated DeviceLinkAttempt
select {
case updated = <-controlPlane.updated:
case <-ctx.Done():
t.Fatal("owner participant was not published")
}
owner := updated.OwnerICE
if owner == nil {
t.Fatal("owner ICE description is missing")
}
if updated.OwnerFingerprint == "" || owner.Ufrag == "" || owner.Pwd == "" {
t.Fatalf("initial owner credentials are incomplete: %+v", updated)
}
peerDescription := authenticatedlink.Description{
Fingerprint: updated.OwnerFingerprint, Ufrag: owner.Ufrag, Pwd: owner.Pwd,
Candidates: append([]string(nil), owner.Candidates...),
}
type connectResult struct {
link *authenticatedlink.Link
err error
}
connected := make(chan connectResult, 1)
go func() {
link, connectErr := caller.Connect(ctx, peerDescription, authenticatedlink.RoleCaller)
connected <- connectResult{link: link, err: connectErr}
}()
controlPlane.mu.Lock()
controlPlane.attempt.CallerICE.Candidates = append(
[]string{},
callerDescription.Candidates...,
)
controlPlane.mu.Unlock()
service.notifyRemoteAttemptChanged("attempt-1")
refetchedCallerCandidates := false
for !refetchedCallerCandidates {
select {
case event := <-diagnostics.events:
refetchedCallerCandidates = event.Stage == "caller_candidate_refetch" &&
event.Outcome == "succeeded"
case <-ctx.Done():
t.Fatal("owner did not refetch trickled caller candidates")
}
}
var link *authenticatedlink.Link
for link == nil {
select {
case result := <-connected:
if result.err != nil {
t.Fatal(result.err)
}
link = result.link
case candidateUpdate := <-controlPlane.updated:
if candidateUpdate.OwnerICE != nil {
caller.AddRemoteCandidates(candidateUpdate.OwnerICE.Candidates)
}
case <-ctx.Done():
t.Fatal("timed out while trickling owner candidates")
}
}
defer link.Close()
if wait := time.Until(attemptExpiry.Add(100 * time.Millisecond)); wait > 0 {
timer := time.NewTimer(wait)
select {
case <-ctx.Done():
timer.Stop()
t.Fatal("timed out while waiting for rendezvous attempt expiry")
case <-timer.C:
}
}
stream, err := link.OpenStream(ctx)
if err != nil {
t.Fatal(err)
}
if err := devicelink.ProbeStream(ctx, stream); err != nil {
t.Fatalf("probe DeviceLink stream: %v", err)
}
if err := writeRemoteFrame(stream, RemoteRequest{
ProtocolEpoch: ApplicationProtocolEpoch, Service: AgentHTTPService,
RequestID: "request-1", Method: http.MethodGet, Path: "/v1/workspaces",
}); err != nil {
t.Fatal(err)
}
var response RemoteResponse
if err := readRemoteFrame(stream, maxRemoteResponseFrameBytes, &response); err != nil {
t.Fatal(err)
}
_ = stream.Close()
if response.Status != http.StatusOK ||
!strings.Contains(string(response.Body), `"workspaceId":"workspace-1"`) {
t.Fatalf("unexpected response: %+v", response)
}
}
func TestRemoteHostAttemptCleanupDoesNotDeleteNewGeneration(t *testing.T) {
t.Parallel()
service := &Service{}
service.remoteHost.attemptWake = NewAttemptWake()
service.remoteHost.attempts = map[string]activeRemoteAttempt{
"attempt-1": {generation: 2},
}
service.remoteHost.attemptWake.Notify("attempt-1")
service.finishRemoteAttempt("attempt-1", 1)
if _, exists := service.remoteHost.attempts["attempt-1"]; !exists {
t.Fatal("old worker cleanup deleted the newer attempt generation")
}
if got := service.remoteHost.attemptWake.Version("attempt-1"); got != 1 {
t.Fatalf("old worker cleanup forgot current wake version: %d", got)
}
service.finishRemoteAttempt("attempt-1", 2)
if _, exists := service.remoteHost.attempts["attempt-1"]; exists {
t.Fatal("current worker cleanup did not delete its attempt")
}
if got := service.remoteHost.attemptWake.Version("attempt-1"); got != 0 {
t.Fatalf("current worker cleanup retained wake version: %d", got)
}
}
func TestRemoteHostAttemptChangeWakesDiscoveryAndRendezvous(t *testing.T) {
service := &Service{}
service.remoteHost.attemptWake = NewAttemptWake()
service.remoteHost.pollWake = make(chan struct{}, 1)
service.notifyRemoteAttemptChanged("attempt-1")
if got := service.remoteHost.attemptWake.Version("attempt-1"); got != 1 {
t.Fatalf("attempt wake version = %d, want 1", got)
}
select {
case <-service.remoteHost.pollWake:
default:
t.Fatal("attempt change did not wake owner discovery")
}
}
func TestRemoteHostDisablesSharedAdmissionUntilIdentityRecovers(t *testing.T) {
t.Parallel()
service := &Service{}
service.remoteHost.managedLinks = make(map[string]remoteManagedLink)
service.remoteHost.observedLinkEvents = make(map[string]uint64)
service.remoteHost.linkManager = service.newRemoteLinkManager()
manager := service.remoteHost.linkManager
service.stopRemoteAttempts(nil)
if _, err := manager.Admit(context.Background(), "pairing-1"); !errors.Is(err, linkmanager.ErrManagerDisabled) {
t.Fatalf("Admit while identity unavailable error = %v, want disabled", err)
}
service.setRemoteLinkEnabled(true)
admission, err := manager.Admit(context.Background(), "pairing-1")
if err != nil {
t.Fatalf("Admit after identity recovery: %v", err)
}
admission.Close()
if err := manager.WaitForQuiescence(context.Background()); err != nil {
t.Fatal(err)
}
}
func TestRemoteHostStartWaitsForInProgressClose(t *testing.T) {
t.Parallel()
service := &Service{}
service.remoteHost.cancel = func() {}
service.remoteHost.managedLinks = make(map[string]remoteManagedLink)
service.remoteHost.observedLinkEvents = make(map[string]uint64)
service.remoteHost.linkManager = service.newRemoteLinkManager()
admission, err := service.remoteHost.linkManager.Admit(context.Background(), "pairing-1")
if err != nil {
t.Fatal(err)
}
closed := make(chan struct{})
go func() {
service.Close()
close(closed)
}()
waitForRemoteHostCondition(t, func() bool {
service.remoteHost.mu.Lock()
defer service.remoteHost.mu.Unlock()
return service.remoteHost.stopping
})
started := make(chan struct{})
go func() {
service.StartRemoteHost(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
close(started)
}()
select {
case <-started:
t.Fatal("StartRemoteHost returned before the previous run closed")
case <-time.After(20 * time.Millisecond):
}
admission.Close()
select {
case <-closed:
case <-time.After(time.Second):
t.Fatal("Close did not finish after admission release")
}
select {
case <-started:
case <-time.After(time.Second):
t.Fatal("StartRemoteHost did not resume after Close")
}
service.Close()
}
func TestControlPlaneUnauthorizedClassification(t *testing.T) {
t.Parallel()
for _, status := range []int{http.StatusUnauthorized, http.StatusForbidden} {
if !isControlPlaneUnauthorized(&ControlPlaneError{StatusCode: status}) {
t.Fatalf("status %d was not classified as unauthorized", status)
}
}
if isControlPlaneUnauthorized(&ControlPlaneError{StatusCode: http.StatusInternalServerError}) {
t.Fatal("server error was classified as unauthorized")
}
}
func waitForRemoteHostCondition(t *testing.T, condition func() bool) {
t.Helper()
deadline := time.Now().Add(time.Second)
for !condition() {
if time.Now().After(deadline) {
t.Fatal("remote host condition was not satisfied")
}
time.Sleep(time.Millisecond)
}
}