package bidder import ( "cmp" "context" "errors" "fmt" "slices" "strings" "sync" "time" "github.com/bluesky-social/indigo/atproto/syntax" commonv1 "github.com/johnandersen777/socialweb-computer-kcp/api/common/v1alpha1" "github.com/johnandersen777/socialweb-computer-kcp/api/v1alpha1" "github.com/johnandersen777/socialweb-computer-kcp/internal/atproto/market" "github.com/johnandersen777/socialweb-computer-kcp/internal/atproto/xrpc" "github.com/johnandersen777/socialweb-computer-kcp/internal/bus" "github.com/johnandersen777/socialweb-computer-kcp/internal/contract/atprotorepo" contract "github.com/johnandersen777/socialweb-computer-kcp/internal/contract/market" "github.com/johnandersen777/socialweb-computer-kcp/internal/contract/pds" "github.com/johnandersen777/socialweb-computer-kcp/internal/contract/policy" "github.com/johnandersen777/socialweb-computer-kcp/internal/contract/vault" "github.com/johnandersen777/socialweb-computer-kcp/internal/guestseed" "github.com/johnandersen777/socialweb-computer-kcp/internal/ingress/webrtc" "github.com/johnandersen777/socialweb-computer-kcp/internal/kit/kcp" "github.com/johnandersen777/socialweb-computer-kcp/internal/kit/pinneddownloads" core "github.com/johnandersen777/socialweb-computer-kcp/internal/market/bidder" "github.com/johnandersen777/socialweb-computer-kcp/internal/market/participant" "github.com/johnandersen777/socialweb-computer-kcp/internal/policyengine/runs" ) func inputsSubscription(inputs *participant.Inputs) contract.Subscription { return contract.Subscription{Kinds: []string{contract.KindExposure, contract.KindMarketIdentity}, Observer: inputs} } func roleSubscription(bidder *core.Bidder) contract.Subscription { return contract.Subscription{Kinds: []string{contract.KindBid, contract.KindComputeVM, contract.KindStateSync}, Observer: bidder, Synced: bidder.Synced} } func (s *Service) resolveIntents(ctx context.Context) (contract.Intents, error) { if s.deps.Bus == nil { return nil, errors.New("bidder: the bus is not available") } self := s.self() return bus.Resolve[contract.Intents](s.deps.Caller(ctx), s.deps.Bus, self, self, bus.Capability{Name: contract.CapabilityIntents}) } func (s *Service) awaitInputs(ctx context.Context) error { var wg sync.WaitGroup ref := s.spec.MarketExposureRef exposure := "" if ref != nil { exposure = strings.TrimSpace(ref.Name) } if exposure != "" { wg.Go(func() { s.marketURL = s.inputs.AwaitAddress(ctx, exposure, inputWait, nil) }) } wg.Go(func() { s.guestURL = s.resolveGuestURL(ctx) }) wg.Go(func() { s.resolveIdentity(ctx) }) wg.Wait() if exposure != "" && s.marketURL == "" && s.inboundXRPC() { return fmt.Errorf("bidder: the market Exposure %s reports no address yet; the offering and the service-auth audience have one source, so the bidder does not advertise another host", exposure) } return nil } func (s *Service) resolveGuestURL(ctx context.Context) string { if !s.publicNetwork() { return kcp.HandlerURL(s.workspace, guestControlKind, guestControlName) } raw := s.inputs.AwaitAddress(ctx, guestExposure, guestExposureWait, participant.AbsoluteHTTPURL) if raw == "" { s.log.Warn("the bidder cannot read the guest control plane ingress URL; guest provisioning fails closed") return "" } s.log.Info("the bidder uses the guest control plane ingress URL", "url", raw) return raw } func (s *Service) resolveIdentity(ctx context.Context) { if s.spec.Identity != nil { s.did = s.spec.Identity.Did } ref := s.identityRef() if s.did != "" || ref == nil || ref.Name == "" { return } found, ok := s.inputs.AwaitIdentity(ctx, []string{s.workspace}, ref.Name, inputWait) if !ok { s.log.Warn("the bidder identity is not resolved yet", "marketIdentity", ref.Name) return } s.did = found.Status.Did } func (s *Service) newBidder(intents contract.Intents) (*core.Bidder, error) { if s.did == "" { return nil, errors.New("bidder: the record store needs the bidder DID") } records, err := participant.NewRepoStore(participant.RepoConfig{DID: s.did, Directory: s.dir, HTTP: s.egress.Client(RecordRequestTimeout), Open: s.openRepo}) if err != nil { return nil, err } if s.stateDir() == "" { return nil, errors.New("bidder: the attestation key needs a state dir") } signer, err := market.LoadOrCreateSignerFile(s.stateDir() + "/attestation.jwk") if err != nil { return nil, err } guestNetwork := s.spec.GuestNetwork.EffectiveFor(s.spec.Policy) ports, err := s.webrtcPorts() if err != nil { return nil, err } peer := webrtc.NewPeer(s.log) s.mu.Lock() s.marketPeer = peer s.mu.Unlock() httpClient := webrtc.Client(s.egress.Client(DefaultCallTimeout), peer) cfg := core.Config{ DID: s.did, Workspace: s.workspace, ClaimedBy: s.deps.Self.Name, ProviderHandle: s.providerHandle, GuestURL: s.guestURL, EndpointURL: s.inboundMarketURL(), InboundXRPC: s.inboundXRPC(), ScopePolicy: s.spec.Policy, GuestTLSPort: portOf(s.spec.Network.GuestTLSURL()), GuestLocalAddress: func() string { return s.inputs.Address(guestLocalExposure) }, GuestDownloads: s.guestDownloads(), GuestVaultServer: s.guestVaultServer, GuestVaultMount: DefaultGuestVaultMount, GuestLoginMount: DefaultGuestLoginMount, GuestLANUDP: guestNetwork.LANUDP, GuestHostUDP: guestNetwork.HostUDP, GuestLANAddresses: s.lanAddresses, MaxConcurrentGuests: s.spec.MaxConcurrentGuests, Heartbeat: seconds(s.spec.HeartbeatIntervalSeconds), OfferingRefresh: seconds(s.spec.OfferingRefreshSeconds), BidTTL: seconds(s.spec.BidTTLSeconds), Suspend: s.spec.SuspendParticipation, Relays: append([]string(nil), s.spec.Network.Relays()...), ComputeKinds: append([]string(nil), s.spec.ComputeKinds...), StateDir: s.stateDir(), Sandbox: s.deps.Sandbox, } if s.spec.Kcp != nil { cfg.StopGraceSeconds = s.spec.Kcp.StopGraceSeconds } return core.New(cfg, core.Deps{ Intents: intents, Records: records, Signer: signer, Vault: s.guestVault(), Guests: guests{}, GuestEvents: s.guestEvents(), WebRTCPorts: ports, Ingress: peer, ScopeGrants: s.scopeGrants, Tokens: participant.NewTokens(s.minter(), DefaultCallTimeout), HTTP: httpClient, Directory: s.dir, Policy: s.policyRuns(), Log: s.log, }) } func (s *Service) guestDownloads() map[string]string { pins := s.blobPins() self, err := guestseed.SelfDownloadOf() if err != nil { s.log.Warn("the bidder does not serve its own binary; a contract with spec.guestTools socialweb-computer fails to provision", "err", err.Error()) return pins } if pins == nil { pins = map[string]string{} } pins[self.Name] = self.Sha256 s.log.Info("the bidder serves its own binary to guests that ask for it", "name", self.Name, "sha256", self.Sha256, "bytes", self.Size) return pins } func (s *Service) blobPins() map[string]string { dir := "" if s.spec.Storage != nil { dir = strings.TrimSpace(s.spec.Storage.BlobsDir) } if dir == "" { s.log.Warn("the bidder names no guest downloads dir; a contract with a tunnel fails to provision") return nil } var required []string for _, name := range []string{guestseed.GuestBinaryOpenTunnel, guestseed.GuestBinaryWootty, guestseed.GuestBinaryAgent} { for _, arch := range guestseed.GuestArches { required = append(required, guestseed.GuestDownloadName(name, arch)) } } pins, err := pinneddownloads.LoadPins(dir, required) if err != nil { s.log.Warn("the bidder cannot read its guest download pins; a contract with a tunnel fails to provision", "dir", dir, "err", err) return nil } s.log.Info("the bidder pins the guest binaries it serves", "dir", dir, "downloads", len(pins)) return pins } func (s *Service) repoRef() commonv1.ObjectRef { return commonv1.ObjectRef{Group: v1alpha1.GroupName, Kind: "AtprotoRepo", Name: atprotorepo.RepoName(s.did), Workspace: s.workspace} } func (s *Service) openRepo(ctx context.Context) (atprotorepo.Repo, error) { return bus.Resolve[atprotorepo.Repo](ctx, s.deps.Bus, s.self(), s.repoRef(), bus.Capability{Name: atprotorepo.CapabilityWrite}) } func (s *Service) awaitRepo(ctx context.Context) { repo := s.repoRef() bound := make(chan struct{}, 1) cancel := s.deps.Bus.Subscribe(func(key bus.Key) { if key.Workspace != repo.Workspace || key.Name != repo.Name { return } select { case bound <- struct{}{}: default: } }) defer cancel() waited := false for { _, err := s.openRepo(ctx) if !errors.Is(err, atprotorepo.ErrNotBound) { if waited { s.log.Info("the bidder repo is bound; the bidder writes its offering", "repo", repo.Name) } return } if !waited { s.log.Info("the bidder waits for its repo binding before it writes its offering", "repo", repo.Name, "err", err) waited = true } select { case <-ctx.Done(): return case <-bound: } } } func seconds(value *int32) time.Duration { if value == nil { return 0 } return time.Duration(*value) * time.Second } func (s *Service) minter() xrpc.Minter { target, ok := s.serviceAuthSigner() if s.deps.Bus == nil || s.did == "" || !ok { return nil } handlers, self, did := s.deps.Bus, s.self(), s.did return participant.PDSServiceAuth{DID: did, Open: func(ctx context.Context) (pds.ServiceAuth, error) { return bus.Resolve[pds.ServiceAuth](ctx, handlers, self, target, bus.Capability{Name: pds.CapabilityServiceAuth, Scope: []string{did}}) }} } func (s *Service) scopeGrants(ctx context.Context) ([]string, bool, error) { target, ok := s.serviceAuthSigner() if s.deps.Bus == nil || s.did == "" || !ok { return nil, false, nil } handle, err := bus.Resolve[pds.ServiceAuth](ctx, s.deps.Bus, s.self(), target, bus.Capability{Name: pds.CapabilityServiceAuth, Scope: []string{s.did}}) if err != nil { return nil, false, err } grants, ok := handle.(pds.ScopeGrants) if !ok { return nil, false, nil } granted, known := grants.GrantedScopes(s.did) return granted, known, nil } func (s *Service) serviceAuthSigner() (commonv1.ObjectRef, bool) { if ref := s.spec.PdsRef; ref != nil && strings.EqualFold(strings.TrimSpace(ref.Kind), "Pds") && strings.TrimSpace(ref.Name) != "" { return commonv1.ObjectRef{Group: v1alpha1.GroupName, Kind: "Pds", Name: ref.Name, Workspace: s.workspace}, true } if s.spec.Identity != nil && s.spec.Identity.MarketIdentityRef != nil && strings.TrimSpace(s.spec.Identity.MarketIdentityRef.Name) != "" { ref := s.spec.Identity.MarketIdentityRef return commonv1.ObjectRef{Group: v1alpha1.GroupName, Kind: "MarketIdentity", Name: ref.Name, Workspace: cmp.Or(ref.Workspace, s.workspace)}, true } return commonv1.ObjectRef{}, false } type guestVault struct { handlers *bus.Bus caller func(context.Context) context.Context self commonv1.ObjectRef target commonv1.ObjectRef scope []string } var _ vault.KV = guestVault{} func (s *Service) guestVault() vault.KV { if s.deps.Bus == nil { return nil } return guestVault{handlers: s.deps.Bus, caller: s.deps.Caller, self: s.self(), target: commonv1.ObjectRef{Group: v1alpha1.GroupName, Kind: "Vault", Name: s.guestVaultServer, Workspace: s.workspace}, scope: []string{DefaultGuestVaultMount + "/" + v1alpha1.GuestVaultPrefix + "/*"}} } func (v guestVault) open(ctx context.Context) (vault.KV, error) { return bus.Resolve[vault.KV](v.caller(ctx), v.handlers, v.self, v.target, bus.Capability{Name: vault.CapabilityWrite, Scope: v.scope}) } func (v guestVault) Read(ctx context.Context, mount, path string, version int) (map[string]any, error) { kv, err := v.open(ctx) if err != nil { return nil, err } return kv.Read(ctx, mount, path, version) } func (v guestVault) Write(ctx context.Context, mount, path string, data map[string]any) error { kv, err := v.open(ctx) if err != nil { return err } return kv.Write(ctx, mount, path, data) } func (v guestVault) Delete(ctx context.Context, mount, path string) error { kv, err := v.open(ctx) if err != nil { return err } return kv.Delete(ctx, mount, path) } func (s *Service) resolvePolicyRunner(ctx context.Context, engine commonv1.ObjectRef, workflow string) (policy.Runner, error) { return bus.Resolve[policy.Runner](ctx, s.deps.Bus, s.self(), engine, bus.Capability{Name: policy.CapabilityRun, Scope: []string{workflow}}) } func (s *Service) policyRuns() *runs.Client { engine := runs.EngineRef(s.spec.PolicyEngineRef, s.workspace) if s.deps.Bus == nil || engine.Name == "" { return nil } return participant.NewPolicyRuns(participant.PolicyConfig{ Resolve: s.resolvePolicyRunner, Owner: policy.Owner{APIVersion: v1alpha1.GroupVersion.String(), Kind: Kind, Name: s.deps.Self.Name, UID: string(s.deps.UID)}, Engine: engine, Role: core.PerspectiveBidder, Prefix: "policy", Args: s.policyArgs, Timeout: seconds(s.spec.PolicyTimeoutSeconds), Parser: runs.Strict, Association: s.spec.Association, }) } func (s *Service) newAuthorizer() *participant.Authorizer { if s.serviceAuthHost == "" { s.serviceAuthHost = participant.HostPort(s.publicMarketURL()) } var audiences []string if s.spec.ServiceAuth != nil { audiences = append(audiences, s.spec.ServiceAuth.Audience...) } audiences = append(audiences, market.ServiceAudienceDIDs(s.serviceAuthHost)...) return &participant.Authorizer{ Directory: s.dir, Hostname: s.serviceAuthHost, ServiceIDs: []string{market.DefaultMarketServiceID, market.DefaultComputeEventServiceID}, AudienceDIDs: audiences, } } const publicPDSLookupTimeout = 10 * time.Second func (s *Service) publicCrawlRelays() []string { network := s.spec.Network.Relays() var out []string for _, relay := range s.spec.Network.DiscoveryRelayURLs() { if !slices.Contains(network, relay) { out = append(out, relay) } } return out } func (s *Service) publicPDS(ctx context.Context) string { if s.inputs.Address(pdsExposure) == "" || s.dir == nil { return "" } did, err := syntax.ParseDID(s.did) if err != nil { return "" } ctx, cancel := context.WithTimeout(ctx, publicPDSLookupTimeout) defer cancel() ident, err := s.dir.LookupDID(ctx, did) if err != nil || ident == nil { s.log.Warn("participant_request_crawl_unresolved", "did", s.did, "err", err) return "" } return ident.PDSEndpoint() } func (s *Service) crawl() participant.Crawl { return participant.Crawl{ Relays: append([]string(nil), s.spec.Network.Relays()...), Address: func() string { return s.inputs.Address(pdsExposure) }, PublicRelays: s.publicCrawlRelays(), PublicAddress: s.publicPDS, Crawled: func(ctx context.Context) { if bidder := s.bidder.Load(); bidder != nil { bidder.TouchOffering(ctx) } }, Period: participant.CrawlPeriod, Log: s.log, } }