~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
394 lines · 26.0 KB Raw History

KCP: how socialweb-computer uses kcp

  • kcp = the state store, API server and composition layer of EVERY application, not shared infra
  • companion of SYSTEM.md; decisions + measurements in ARCHITECTURE.md sections 0, 6, 12, 13
  • source of truth for types: api/*/v1alpha1; for schemas and exports: generated files under deploy/

kcp Is Part Of Each App

  • application instance = its own kcp + a manifest set + a profile
  • application state = kcp objects (ComputeVM, ComputeContract, NodeClaim, OpenBaoServer, HostProcess, component kinds, Service, Exposure, ...)
  • components run in process in the core; third-party binaries are HostProcess units; both reconciled from objects
  • apps talk to each other ONLY over the atproto market protocol. a shared kcp is composition + isolation, never a data channel
  • only exception: tenant Kubernetes clusters of the Gateway use kcp the traditional way (home workspaces, bound exports, tenant k3s behind kcp)

Which kcp, by profile

profilekcpworkspaces
prod gateway (install gateway)strict kcp per app, blue/green slotsapp root in root, provider in root:socialweb-provider, requester side in root:app, cluster homes
prod ssh gateway (run gateway ssh, standalone)its own strict kcp on the gateway hostapp root in root
prod bidder (install bidder, per handle)strict kcp per handle, one slotapp root in root
CLI requester (run request without --network)fresh kcp per invocation, state dir persistsapp root in root
market network (dev up --profile market-network, CI, soak)ONE kcp for every appnetwork in root, shared services root:global root:relay root:alice root:bob, each app root:<app>, its components root:<app>:app, child cores in root:dev:<instance>-<app>
browser page (kcp fork, js/wasm)in-tab kcp, memory storagerequester root
iOS app (pkg/embedded)in-process kcp + vaultrequester root; snapshot bytes hold vault secrets in clear: store encrypted, never log
  • one manifest set serves both shapes: in the network the applier rebases every phase workspace under root:<app> (kcpapply.Options.Base); root:*-provider and sets of scope kcp stay shared
  • service names in manifests: <name>.default.{{.ServiceSuffix}}
  • a child app in a network has NO kcp or kine of its own; its core is a HostProcess placed by the network's devsup, scoped to root:<app>
  • children learn the network from values copied onto their root objects and valueFrom refs to other workspaces

The kcp Runtime: Slots, Front, Kine, Postgres

piecegatewaybiddermarket network / dev
kcp binaryunpatched kcp-dev/kcp main, pinned asset (versions.json assets.kcp)samesame
slotsblue/green socialweb-kcp-gw@a, @bone slot per handle socialweb-kcp-<hid>@aone slot
storekine over postgres unit socialweb-kcp-gw-pg (unix socket)kine over sqlitekine
frontown L4 front socialweb-kcp-gw-front on 127.0.0.1:6443 (caddy -> kcp.socialweb.computer)port-0 frontfront under devsup
upgraderoll: new slot beside old on same store, front flips after /readyz, old exits, Lease kube-system/kcp-controllers deleted so new slot leads at once; pg_dump before every rollshort restartdev roll kcp
  • slots run --enable-leader-election and --shard-base-url = the front
  • kcp release pairs whose storage versions or bootstrap schemas differ: planned short outage instead of a roll
  • caddy stays public TLS only, never reconfigured by a swap
  • KcpInstallation records the selected slot and the front backend
  • operator commands: units roll kcp (installed host), dev roll kcp (dev), dev restart <instance> core
  • memory caps: kcp slot 3 GiB (kcpserver.SlotMemoryMax), front 256 MiB, kine kcpdatastore.KineMemoryMax
  • packages: internal/kcpserver (slots, kubeconfigs, member tokens, roll), internal/kcpdatastore (kine), internal/apiserverfront (fronts), internal/lease/local (slots, ports, rolls)
  • tenant clusters reuse the pattern: per-cluster k3s slots a/b over kine over that cluster's postgres, behind a per-cluster front (controlplane.json records activeSlot)

Workspaces, APIExports, APIBindings, WorkspaceTypes

  • provider workspace root:socialweb-provider (type universal) holds every APIResourceSchema and APIExport
  • apps bind the exports they need with APIBindings (spec.reference.export.path: root:socialweb-provider)
  • WorkspaceTypes give new workspaces default bindings

Exports

exportresources (first ones)bound by
applicationsMarketNetwork, MarketBidder, MarketRequester, Gateway, SshGateway, component kinds, repos, leases, StateSyncroot (applications-binding.yaml), socialweb-application type
clustersCluster, Node, NodeClaim, ClusterLink, ClusterClientCertificatecluster type, socialweb-application type
atprotoidentitiesAtprotoIdentityaccount type, socialweb-application type
marketidentitiesMarketIdentity, AtprotoRecord, AtprotoRecordWatchgateway root:app (deploy/apps/gateway), application type
computevmsComputeVM, FirecrackerVM, Bid, GuestIdentityGrantbidder type, root (computevms-binding.yaml)
computecontractsComputeContract, ContractEvent, Infra, EphemeralInfra, InfraCollection, EphemeralInfraCollectionrequester and gateway bindings
hostprocessesHostProcess, Postgres, OpenBaoServer, RemoteOpenBao, Vaultroot (hostprocesses-binding.yaml), bidder type
eventsEventStream, EventSource, Pds/Relay/JetstreamEventSourceroot (events-binding.yaml)
exposuresService, Exposure, ComponentLinkroot (exposures-binding.yaml)
pdsRemotePDSroot (pds-binding.yaml)
policyPolicyEngine, PolicyWorkflow, PolicyWorkflowRun, PolicyTriggerroot (policy-binding.yaml)

WorkspaceTypes

typedefault bindingschild typeused for
accountatprotoidentitiesclustergateway account home (DID to shard map)
clusterclusterstenant cluster home workspace
socialweb-applicationall eleven exportsan application workspace
biddercomputevms, hostprocessesbidder workspace
dev-instances / dev-instancedev-instance onlyroot:dev and root:dev:<instance> of a dev host
  • never use kcp's shipped home (~): a bare LogicalCluster that can never be deleted
flowchart TD
  subgraph ROOT["workspace root"]
    APPROOT["app root object: MarketBidder / Gateway / ..."]
    B1["APIBinding applications"]
    B2["APIBinding computevms"]
    B3["APIBinding hostprocesses, events, exposures, pds, policy"]
    WT["WorkspaceTypes: account, cluster, socialweb-application, bidder"]
  end

  subgraph PROV["workspace root:socialweb-provider"]
    ARS["APIResourceSchema v1alpha1-REV.plural.group (immutable)"]
    EXP["APIExports: applications, clusters, computevms, computecontracts, hostprocesses, marketidentities, atprotoidentities, events, exposures, pds, policy"]
  end

  subgraph ACC["account workspace (type account)"]
    AID["AtprotoIdentity"]
    subgraph CLW["cluster home (type cluster)"]
      CL["Cluster, NodeClaim, ClusterLink"]
    end
  end

  ARS --> EXP
  EXP -->|"bound by"| B1
  EXP -->|"bound by"| B2
  EXP -->|"bound by"| B3
  WT -->|"defaultAPIBindings"| ACC
  WT -->|"defaultAPIBindings"| CLW

Manifest sets

  • deploy/sets.yaml: named sets of ordered phases; each phase = workspace + files + after waits (Workspace Ready, APIExport IdentityValid, APIBinding Ready)
  • sets: provider (scope kcp: provider workspace, schemas, exports, root bindings, WorkspaceTypes), bidder, hostprocesses, dev-host, grants
  • per-app sets: deploy/apps/{gateway,market-bidder,market-requester,ssh-gateway,repo-pds} (vault, RBAC, extra bindings, iroh dialer, association watch)
  • market network: deploy/market-network/ (workspaces, network vault, peers RBAC)
  • applied by socialweb-computer apply / internal/controllers/runtime/kcpapply
  • every app root re-renders its set's HostProcesses and OpenBaoServers each pass; a missing or Lost one voids the applied-set shortcut and the set is applied again

The Component Runtime

  • one core process per instance (internal/stack/componentcore) = controller manager + component runtime + HostProcess supervisor
  • component kinds (Bidder, Requester, Pds, Plc, Relay, GuestControlPlane, IngressProxy, DispatcherTLS, GatewayAPI, SshServer, PolicyEngine) are objects; internal/controllers/component reconciles each into a running instance in internal/component
  • a component never listens itself: it registers handlers; a Service names component + handler; an Exposure publishes the Service
  • local Exposure port comes from the core's port registry (<state>/ports/ports.json, 64-port block of 20000-29999 by state dir), survives a core restart, reported in status.address
  • HostProcess = a systemd user unit (or proc backend) for a third-party binary; followed via D-Bus signals; adopted across core restarts by unit name + InvocationID
  • what a HostProcess runs: kcp slots, kine, postgres, OpenBao, fronts, dumbpipe/iroh dialer, firecracker via socialweb-vmexec, child cores (market network), deno for policy
  • suspend: root spec.suspend stops participation, never a process; per component spec.components[].suspend stops that process
flowchart TD
  ROOT["App root: MarketBidder"] -->|"composes, SSA socialweb.computer/marketbidder"| MI["MarketIdentity bidder"]
  ROOT --> BAO["OpenBaoServer"]
  ROOT --> VAULT["Vault (abstract) backendRef -> OpenBaoServer"]
  ROOT --> PDS["Pds component"]
  ROOT --> BIDC["Bidder component"]
  ROOT --> GCP["GuestControlPlane component"]
  ROOT --> PE["PolicyEngine"]
  ROOT --> ING["IngressProxy"]
  ROOT --> ES["EventSource + EventStream"]
  ROOT --> LINKS["ComponentLinks"]
  BIDC -->|"creates"| BID["Bid"]
  BID -->|"owns"| CVM["ComputeVM (abstract)"]
  CVM -->|"backendRef"| FVM["FirecrackerVM"]
  FVM -->|"runs as"| HP1["HostProcess: vmexec + firecracker"]
  BAO -->|"runs as"| HP2["HostProcess: openbao"]
  PE -->|"runs"| HP3["deno sandbox"]
  BIDC --> SVC["Service"]
  SVC --> EXPO["Exposure: status.address"]

Controllers Pattern

  • one reconciler per kind: internal/controllers/<kind> on internal/controllers/runtime (the only home of the dynamic client and unstructured)
  • kcp multicluster: every core runs the manager for its own workspaces; a root reconciles only roots of its core's base workspace
  • no polling. driven by watches on own kind + every kind it refs
    • deadlines: delayed requeue
    • errors: rate-limited backoff
    • ticker only for an outbound obligation listed in deploy/obligations.txt (owner + period)
    • systemd units via D-Bus signals, processes via wait
  • writers rule (every write = server-side apply, field manager socialweb.computer/<controller>):
    • a spec is written only by its composer
    • a status only by the controller of its kind
    • a field another controller reports = its own status subfield + own field manager (e.g. ComputeVM status.market.guest by guestcontrolplane)
    • every child/intent object has an ownerReference to its composer or creator
    • no annotation carries a control signal (release = spec.release, restart = spec.restartedAt)
    • a child edited by another manager is re-applied with force and a ChildEditReverted Warning Event
  • cross-workspace children cannot carry ownerReferences: they carry root labels (apps.socialweb.computer/root, root-kind, root-cluster), pruned by label, torn down before the root's finalizer goes
  • abstract kinds (AtprotoRepo, Vault, ComputeVM, EventSource): spec.backendRef to one concrete kind with label contracts.socialweb.computer/<abstract>; abstract status mirrors backend; conditions Ready + BackendBound
  • gate: scripts/check-architecture.sh (hack/archcheck) enforces layers, obligations, bus callers, no GVR literals outside api/, no annotation signals, apigen:"userEndpoint" on spec endpoints

Bus and ComponentLinks

  • bus.Resolve[T](ctx, b, from, to, capability) -> typed contract handle
  • caller = the reconciled object or the component instance, never a controller; System only for deploy/bus-bootstrap-callers.txt
  • the callee holds credentials; a handle never exposes a token
  • a call with no Accepted ComponentLink fails ErrLinkDenied
  • ComponentLink lives in the target's workspace: spec.from (ref or selector), spec.to, spec.capabilities (name + scope), spec.suspend
  • capabilities: xrpc.call, stream.dial, policy.run, vault.*, repo.*, pds.*, pds.serviceauth, events.*
  • roots generate links from component refs in their desired specs (internal/app/compose/links.go) and wait for Accepted before calling
  • bound signals: NotifyBound (e.g. vault provider) wakes waiters without polling; binding fingerprint includes readiness
sequenceDiagram
  autonumber
  participant K as kcp
  participant RT as controller runtime
  participant C as controller (kind X)
  participant BUS as bus
  participant L as ComponentLink
  participant T as target component
  K-->>RT: watch event (X or a ref'd kind)
  RT->>C: Reconcile(X)
  C->>K: read X + refs
  C->>BUS: Resolve[T](from=X, to=target, cap)
  BUS->>L: accepted link admits cap?
  alt no accepted link
    BUS-->>C: ErrLinkDenied
  else admitted
    BUS-->>C: typed handle
    C->>T: contract call (callee holds creds)
    T-->>C: result
  end
  C->>K: SSA status (own field manager)
  C-->>RT: done / requeue after deadline / backoff on error

StateSync and remote access

  • StateSync = durable outbox: source, sink, direction, payload, idempotencyKey, conflict + deletion policy (Retain, DeleteRemote, AckThenDelete), ack, retry
  • observe is the authority; Acked only when the observation matches the desired hash; status never holds secrets or payload bytes
  • examples: bidder vm.terminated before teardown (finalizer waits for Acked); market inbox syncs pruned at 10 min
  • RemoteOpenBao: vault access separate from a server's lifecycle; probes health/seal, authenticates, checks mounts; never writes the remote vault

API Resource Catalog

  • every kind is cluster-scoped (kcp workspaces are the isolation; apigen writes scope: Cluster)
  • schema name: v1alpha1-<rev>.<plural>.<group>; rev = value in resources.go when this doc was written (source of truth: the file)
  • "composer" = who writes spec; status is always the kind's own controller unless noted
  • "operator-applied" = has a controller but no Go composer: applied from manifests or kubectl

App roots (group socialweb.computer, export applications)

kindrevpurposecomposer
MarketNetwork31a whole local market in one kcp: shared services + child rootsdev up
MarketBidder43bidder app: profile, identity, comms, ingress, policy, guestNetwork, firecracker, kcpinstall bidder / run bidder / network
MarketRequester37requester app: identity, comms, secretsFallback, atircParentRef, ingressrun request / page / iOS / network
Gateway38hosted k8s: webAppFront, clusterProvider, operator, marketNetworkinstall gateway / network
SshGateway35ssh as yourself: ssh options, owner sessions, per-user requestersrun gateway ssh / network
ChildApplication2placement of a child app in a network (networkRef, app, profile)operator-applied (no Go composer)

Components (socialweb.computer, export applications)

kindrevpurposecomposer
Bidder24market bidder participantMarketBidder
Requester29contract runner (RFP, accept, secrets, atirc, release)MarketRequester, Gateway, SshGateway
GuestControlPlane11guest endpoints at 192.0.2.1 (events, downloads, identity proxy)MarketBidder
Pds12local PDS (localRepo identities, network PDSes)roots
Plc11local PLC directoryMarketNetwork
Relay11local atproto relayMarketNetwork
IngressProxy12did:key / iroh / opentunnel ingress to the app's XRPCroots
DispatcherTLS11TLS front for the network dispatcherMarketNetwork
GatewayAPI16k8s.socialweb.computer web app + tenant APIGateway
SshServer17ssh server, web sign-in, keys APISshGateway
MarketPresence2bidder offering presence with TTLBidder; adopted by its root

Market and contracts

kindgroup / exportrevpurposecomposer
ComputeContractsocialweb.computer / computecontracts31one rental: spec (vm, tunnels, secretsRef, agent, association, policy), status.contract flow stateCLI, page, Infra, ssh session, NodeClaim
ContractEvent/ computecontracts5one guest/requester event (sealed signals, secrets), owned by its contractCLI, page (outbound); Requester (inbound)
Infra/ computecontracts13durable set of contracts + nodes + secrets, mirrored to computer.socialweb.infraowner, NodeClaim controller
EphemeralInfra/ computecontracts13Infra with TTL / endWhenowner
InfraCollection/ computecontracts1one Infra child per computer.socialweb.infra record in a repoowner
EphemeralInfraCollection/ computecontracts1same for ephemeralInfraowner
Bidsocialweb.computer / computevms9one bid per RFP (name = bid rkey); owns its ComputeVM; status by bidderBidder
ComputeVM/ computevms17abstract VM: size, owner, requester, userData ref, contract refs, releaseBidder
FirecrackerVM/ computevms7concrete firecracker backend (unit, network, image)ComputeVM controller
GuestIdentityGrant/ computevms3guest workload identity grant (entity, api, scopes, claims)Bidder, GuestControlPlane
StateSyncsocialweb.computer / applications1durable outbox (vm.terminated, market inbox)roles, controllers

Identity, repos, records

kindgroup / exportrevpurposecomposer
MarketIdentitysocialweb.computer / marketidentities15app identity: mode (localRepo, pdsAccount, oauthQr, oauthBrowser, oauthDelegated, oauthSessionProxy), associations, pendingActionroots, SshGateway session owners
AtprotoRecord/ marketidentities7one record to keep in a repo (collection, rkey, swapCid, deletionPolicy)roots, Infra
AtprotoRecordWatch/ marketidentities7watch rkeys of a collection (associations, vouches)app sets (repo-pds)
AtprotoIdentitysocialweb.computer / atprotoidentities2DID to identity mapping in an account homegateway
AtprotoRepo/ applications4abstract repo: did, head, rev, writable, addressroots
LocalRepo/ applications2repo on a Pds component (signer handle, no token)roots
RemoteRepo/ applications2repo on a RemotePDS via a MarketIdentity leaseroots
AtprotoSession/ applications2a session of an account on a PDSroots (compose/repo.go)
PdsAccount/ applications2an account on a Pds componentroots
PlcIdentity/ applications1a DID on the local Plcroots
RemotePDSpds.socialweb.computer / pds3a remote PDS endpoint + auth + capabilitiesroots

Vault and host

kindgroup / exportrevpurposecomposer
HostProcesssocialweb.computer / hostprocesses4a host unit for a third-party binary; status.listenersroots, controllers
Postgres/ hostprocesses1embedded postgres (kcp store, cluster store, vault store)gateway, cluster provider
OpenBaoServer/ hostprocesses11local OpenBao: mounts, policies, clients; unseal keys in a 0600 file, root token revokedroots
RemoteOpenBao/ hostprocesses4access to an existing OpenBao (token / AppRole, https)roots
Vault/ hostprocesses1abstract vault: address, ready, caBundleroots
VaultCredential/ applications2a credential held in a vault for a repo accountroots (compose/repo.go)
VaultCertificate/ applications2a PKI certificate (vault, mount, role, commonName)operator-applied
KcpInstallation/ applications1selected kcp slot + front backendoperator-applied
HostInstall/ applications1installed account, version, profileoperator-applied
HostRelease/ applications1installed release version + pruneoperator-applied
PortLease/ applications2a leased port (cluster, purpose, port)MarketNetwork
SubnetLease/ applications2a leased subnet (cluster, purpose, prefix)operator-applied

Tenant clusters (export clusters)

kindrevpurposecomposer
Cluster5mount object: status.URL, phase, Draining, nodeExpirycluster provider (from XRPC)
Node2one node; the bound on leaksnode provider
NodeClaim16one market node for a cluster; status.contract mirrors the ComputeContractcluster provider (spec), NodeClaim controller (status)
ClusterLink2iroh listener in front of a cluster apiserver or vaultcluster provider
ClusterClientCertificate2client cert record per DID (serial, notAfter, revoked)gateway
ClusterRoll2control-plane roll of a cluster (strategy, maxUnavailable)operator-applied

Exposure (group exposure.socialweb.computer, export exposures)

kindrevpurposecomposer
Service2names a component + handler (+ protocol, auth)roots
Exposure5publishes a Service: local port, hostname/path, ingress; status.addressroots
ComponentLink3admits bus calls from -> to with capabilitiesroots (compose/links.go)

Events (group events.socialweb.computer, export events)

kindrevpurposecomposer
EventSource1abstract source: cursor, lag, addressroots
PdsEventSource1subscribeRepos of an AtprotoReporoots
RelayEventSource1a relay subscribeRepos (Relay ref or URL)roots
JetstreamEventSource1a Jetstream (v1 or v2)roots
EventStream7filter (collections, dids) + cursor + sink over one EventSource; spec.resume Live / Recentroots, requester composer
RelayStatus2 (socialweb.computer)status of a Relayroots (compose/repo.go)

Policy (group policy.socialweb.computer, export policy)

kindrevpurposecomposer
PolicyEngine3the engine component: limits, trust, cache, sandbox, bundled actionsroots
PolicyWorkflow2a workflow (inline or record ref) + defaultsroots
PolicyWorkflowRun3one decision run: perspective, self/subject/counterparty DIDs, rfp/bid/accept, cacheroles
PolicyTrigger2run a workflow on matching source eventsroots

Schema Revisions

  • kcp holds APIResourceSchema.spec IMMUTABLE: an update under the same name is Forbidden
  • so each schema is named v1alpha1-<Revision>.<plural>.<group>; Revision per kind in api/*/v1alpha1/resources.go
  • deploy/apiresourceschema-revisions.txt = revision lock: every name ever generated + sha256 of its spec, append-only, merge=union
  • change a Go type without bumping Revision -> generation fails -> deploy/check.sh fails before any host sees it
  • migration = the manifest set applies the new schema, then the re-pointed export; already-bound APIBindings move to the new schema in seconds, identity hash unchanged, stored objects untouched. no binding deleted
  • old schemas stay in kcp, harmless

Burned revisions

  • a revision applied to a prod kcp is taken forever, even if the branch that made it is reverted
  • reverted branch -> its lock line is gone -> the generator may hand out the same number with a different spec -> prod kcp refuses the apply -> core crash loop
  • known burned on prod:
    • Requester v1alpha1-28 (lock now records 28 and 29 with the same spec)
    • MarketRequester v1alpha1-38 (NOT in the lock: the next MarketRequester bump must be 39)
  • rule: before a bump, check the burned list; after a revert of a shipped schema, keep its lock line
  • fix for a crash loop: roll back the binary, bump to a fresh revision, roll forward

apigen And Codegen

flowchart LR
  T["api/GROUP/v1alpha1 Go types + resources.go"] --> G["go generate ./api/..."]
  G --> CG["hack/codegen.sh: zz_generated.deepcopy.go"]
  G --> AG["cmd/socialweb-apigen -root . (internal/apigen)"]
  AG --> S["deploy/KIND-apiresourceschema.yaml"]
  AG --> E["deploy/EXPORT-apiexport.yaml"]
  AG --> L["deploy/apiresourceschema-revisions.txt"]
  AG --> R["zz_generated.register.go, zz_generated.conditions.go"]
  S --> SET["deploy/sets.yaml phases"]
  E --> SET
  SET --> KCP["kcp: root:socialweb-provider"]
  L --> CHK["deploy/check.sh: regen + diff"]
  • Go types are the ONLY source of every schema; nothing else writes the outputs
  • mapping (no kubebuilder markers: the no-comments rule):
    • struct layout from encoding/json tags; required = no omitempty unless apigen:"optional"
    • enums from source: typed string constants in declaration order; a named string type with no constants = error
    • struct tags: apigen:"enum=a|b", default=, minimum=, userEndpoint (required on endpoint fields in spec)
    • metav1.Condition items open; unknown fields are an error in apigen.Model.Validate
  • abstract contracts: APIResource.Contracts adds the label contracts.socialweb.computer/<abstract> to the concrete schema
  • printer columns from APIResource.PrinterColumns
  • checks: deploy/check.sh --fast regenerates into a scratch dir and diffs every file socialweb-apigen -root . -list names; go generate ./... must leave api, internal/atproto/market, deploy unchanged
  • rebase conflicts: scripts/regen-after-rebase.sh; never hand-merge generated output; resolve generator inputs by hand first
  • lexicons: internal/lexgen generates market record Go from lexicons/

See Also