~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
185 lines · 15.1 KB Raw History
plan: "0018"
title: "An application owns its PDS: the PDS is a DenoPod in the application's own kcp and its records are kcp objects"
status: done
note: >
  Shared branch: task/own-pds on GitHub origin. The shared pieces (the AtprotoRecord kind, the repo-pds
  DenoPod manifest set deploy/apps/repo-pds, the hono-repo-pds entry and its record reconciler, the
  association flow) land there first; the bidder side (M1) and the gateway requester side (M2) build on
  them on the same branch. Rebase on it often. Pushes: a58553ac (AtprotoRecord kind, deploy/apps/repo-pds,
  RepoPds values, bidder profiles map to localRepo, the core keeps the repo key in the bidder's OpenBao),
  2fbeca24 (hono-repo-pds), 9f4e8508 (bidder side), 6dd6e1be (canonical association shape), 37cb52d0
  (RepoPds.VaultMountClient), 5370e006 (no serviceAccount); later shas are in the git log of task/own-pds.
source: >
  2026-10-17: Bob's bsky.social account (bobvmbuilder) hit the hosted PDS cap of 35000 write points per
  24 hours, and every prod contract then failed with 429. The prod bidder (profile prod-qr, MarketIdentity
  mode oauthQr) wrote every market record (Bid, bids.free, wif.simple, receipts, events, payloads) into
  Bob's own bsky.social repo. docs/ARCHITECTURE.md section 15 "Bidder PDS write budget" cut the cost per
  contract from 35.4 to 23.4 points; that only moves the cap. The cap belongs to Bob's account, not to the
  bidder.
principle: >
  An application that writes market records owns the repo they live in. The repo is a PDS DenoPod in the
  application's own kcp, rendered from the application's root object by its manifest set, bound to port 0
  and found by service name (pds.default.app.svc.kcp.local). Its key is in the application's own OpenBao.
  Every record the application declares (an association, a vouch, any operator-declared record) is an
  AtprotoRecord object in that kcp, and the PDS DenoPod reconciles the object into the repo. The
  operator's real account is written once, to associate the application's DID with it. The RFP flow is
  unchanged: the PDS only changes where the records live.
state_rules:
  - Read AGENTS.md first. status is one of todo, doing, done, blocked. The lead flips statuses.
items:
  - id: M0-SHARED-PDS-DENOPOD
    title: "The shared pieces: the AtprotoRecord kind, the repo-pds DenoPod, its record reconciler, the association flow"
    depends_on: []
    decision: >
      api/v1alpha1 AtprotoRecord (export marketidentities, so the existing marketidentities binding covers
      it): spec {repo (the PDS DenoPod name, pds), collection, rkey, record (an open object)}, status {phase
      Pending|Applied|Failed, observedGeneration, did, uri, cid, message, conditions (ReferencesResolved)},
      finalizer atprotorecord.socialweb.computer/repo. Records live in git, in the application's manifest
      set (deploy/apps/<app>/records.yaml), applied with the profile like any object. Two substitutions
      only: render-time Go template values of the root object ({{.Kcp.Server}} and the like), and runtime
      references: a record value that is exactly {valueFrom: {kind or resource, name, fieldPath, workspace?,
      namespace?, apiVersion?}} is the field of that kcp object (kind MarketIdentity, AtprotoRecord, DenoPod or
      any socialweb.computer kind; resource names the plural, apiVersion an outside kind; workspace absent is
      the record's own workspace, set it is read through /clusters/<workspace>; fieldPath is dotted with
      [n] indexes). The reconciler keeps the record Pending with condition ReferencesResolved False, reason
      MissingReference, naming every missing or unreadable reference (404, 403), writes it once all
      resolve, and rewrites it when a referenced value changes. That is the ordering: objects wait on each
      other's status. deploy/apps/repo-pds/pds.values.yaml renders the DenoPod pds from manifests.RepoPds
      (field Pds of a set's values; any set can render it), deploy/apps/repo-pds/rbac.yaml its account
      pds. The entry hono-repo-pds reads the repo key from the application's OpenBao (the core creates it
      once), registers the did:plc, serves the repo on port 0 and on the did-key-ingress-proxy subdomain of
      its key, accepts writes only from a service-auth token signed by the repo key, and reconciles the
      AtprotoRecord objects whose spec.repo is pds. associateConfirm decides with the MarketIdentity spec
      (associateWith, approved and rejected lists), records an approval in spec.approvedAssociations, and
      answers once a declared record {challenge: caller, keyId: own DID, service} is in the repo; the
      bidder's record is deploy/apps/market-bidder/records.yaml (challenge from
      spec.approvedAssociations[0], keyId from status.did of MarketIdentity bidder).
    files: ["api/v1alpha1/types_atprotorecord.go", "deploy/apps/repo-pds", "ts/atproto-market/hono-repo-pds", "ts/atproto-market/lib/hono-factory-repo-pds", "ts/atproto-market/lib/atproto-record-kcp"]
    done_when: "a market network bidder runs with the pds DenoPod; deleting an AtprotoRecord object deletes its record from the repo; the pds DenoPod reports its listeners in its status."
    status: done
  - id: M1-BIDDER-OWN-PDS
    title: "Bob's bidder writes its market records to its own PDS DenoPod"
    depends_on: [M0-SHARED-PDS-DENOPOD]
    decision: >
      Every MarketBidder profile (prod-qr, market-network, dev-local) maps to MarketIdentity mode localRepo,
      and the market-bidder set renders pds@<profile>. The bidder DenoPod uses the pds DenoPod by service
      name for every repo read and write; the in-process PDS of the bidder is gone. The MarketIdentity
      associateWith is the bidder's handle, so the scan by that account (bobvmbuilder) is approved without
      a second step. In the market network the core plays the operator's browser once: it signs in as the
      bidder's account on the network's bob PDS and runs the same associateConfirm call.
    done_when: "on a market network, run request ends exec ok against a bidder whose records are all in its pds DenoPod, and the operator account's PDS takes zero writes for the contract."
    status: done
  - id: M2-GATEWAY-REQUESTER-OWN-PDS
    title: "The gateway's requester (Alice) writes its market records to its own PDS DenoPod"
    depends_on: [M0-SHARED-PDS-DENOPOD]
    decision: >
      Before: the gateway's requester DenoPod ran in accounts mode (--account-custody-mount atproto): every
      ComputeContract was carried with the OAuth session of the account named by spec.ownerDid, held by the
      atproto auth mount of the root vault, so every rfp, compute.vm, accept and rbac record of every node
      went into alice's bsky.social repo, and a gateway with no web app session for alice failed every claim.
      Now: the gateway set renders pds@<profile> (deploy/apps/repo-pds) in root:app for the MarketIdentity
      requester (mode localRepo, associateWith = Gateway spec.operator); the core writes the repo key once at
      requester/identity/requester/keys of the root vault, the pds client reads it, and the pds account may
      get the root-vault OpenBaoServer. The requester DenoPod takes --repo-pds-url
      http://pds.default.app.svc.kcp.local: it signs with the repo key and every record goes to that repo; it
      answers bids and events on a callback DID of its own key (identity/requester/callback). spec.ownerDid
      stays the account of a contract for the per-account limits; the orphan scan reads the one repo. The
      web app's sign-in of people (k8s.socialweb.computer, the atproto auth mount) is unchanged. In a market
      network the gateway child's operator is the tenant cluster's owner account on alice's PDS, and the core
      associates once with it, as the bidder's core does. The requester's policy reads alice's vouches
      through the association record (delegated trust), so the repo needs no vouch records of its own.
    done_when: "a prod contract posts no record to alice's bsky.social repo."
    status: done
  - id: ACC18-PDS-IS-A-DENOPOD
    title: "The PDS is a DenoPod in the application's own kcp"
    depends_on: [M1-BIDDER-OWN-PDS]
    done_when: "units list / the MarketBidder status shows the DenoPod pds Running with its listeners; the bidder finds it as pds.default.app.svc.kcp.local."
    status: done
  - id: ACC18-ZERO-OPERATOR-WRITES
    title: "Zero writes to the operator's bsky.social repo per contract"
    depends_on: [M1-BIDDER-OWN-PDS]
    done_when: "the operator account's repo has the same record count before and after a contract (market network bob PDS; prod bobvmbuilder on bsky.social)."
    status: done
  - id: ACC18-KEY-SURVIVES
    title: "The repo key and the DID survive a restart and an upgrade"
    depends_on: [M1-BIDDER-OWN-PDS]
    done_when: "the bidder's DID is the same after a restart of the bidder core and after `socialweb-computer upgrade`; the key is read from the bidder's OpenBao, never created twice."
    status: done
  - id: ACC18-ASSOCIATION-ONCE
    title: "The association is done once and the requester checks it"
    depends_on: [M1-BIDDER-OWN-PDS]
    done_when: "one association write to the operator's repo per bidder DID; a restart does not ask again; the requester's policy resolves the bidder's operator from the association record (requester-tangled-vouch)."
    status: done
  - id: ACC18-GATEWAY-SAME-PATTERN
    title: "The gateway requester uses the same PDS DenoPod pattern"
    depends_on: [M2-GATEWAY-REQUESTER-OWN-PDS]
    done_when: "the gateway's kcp shows its own pds DenoPod and AtprotoRecord objects; alice's bsky.social repo takes no write per contract."
    status: done
  - id: ACC18-GATEWAY-PDS-IS-A-DENOPOD
    title: "The gateway's PDS is a DenoPod in the gateway's own kcp"
    depends_on: [M2-GATEWAY-REQUESTER-OWN-PDS]
    done_when: "the Gateway status shows the DenoPod pds Running with its listener next to socialweb-computer-k8s and requester; the requester finds it as pds.default.app.svc.kcp.local."
    status: done
  - id: ACC18-GATEWAY-ZERO-OPERATOR-WRITES
    title: "Zero writes to alice's repo per contract"
    depends_on: [M2-GATEWAY-REQUESTER-OWN-PDS]
    done_when: "the operator account's repo has the same rev and record count before and after a node contract (market network alice PDS; prod alice on bsky.social)."
    status: done
  - id: ACC18-GATEWAY-KEY-SURVIVES
    title: "The gateway requester's repo key and DID survive a restart and an upgrade"
    depends_on: [M2-GATEWAY-REQUESTER-OWN-PDS]
    done_when: "the Gateway status did is the same after a restart of the gateway core and after `socialweb-computer upgrade`; the key is read from the root vault, never created twice."
    status: done
  - id: ACC18-GATEWAY-ASSOCIATION-ONCE
    title: "Alice associates the gateway requester DID once"
    depends_on: [M2-GATEWAY-REQUESTER-OWN-PDS]
    done_when: "one association write to alice's repo per requester DID; a restart does not ask again; a bidder under bidder-tangled-vouch resolves alice as the requester's operator from the association record."
    status: done
  - id: M3-OPERATED-BIDDER-DISCOVERY
    title: "A requester finds the bidders its vouched operators operate"
    depends_on: [M1-BIDDER-OWN-PDS, M2-GATEWAY-REQUESTER-OWN-PDS]
    decision: >
      A bidder DID made by its own PDS is in no relay index and in no vouch record, so a requester that knew
      Bob's account found no bidder. Discovery adds, for every DID the requester vouches (its own and,
      through its association, its operator's), the DIDs that DID's repo associates (a badgeBlueKeys record
      bidder_associate {challenge: the vouched DID, keyId: the bidder}, the one record the QR page or the
      core writes into the operator's repo) whose own repo names that operator back with the same canonical
      record (discoverOperatedDids in operator-discovery-badge-blue-keys). bidderDids stays an extra source.
    done_when: "a gateway with no bidderDids rents a node from a bidder that only its operator's vouched account operates."
    status: done
  - id: M3-RECORDS-BEFORE-PDS-STOPS
    title: "Deleting an application removes its AtprotoRecord objects before its pds DenoPod stops"
    depends_on: [M0-SHARED-PDS-DENOPOD]
    decision: >
      The MarketBidder and Gateway delete paths delete every AtprotoRecord object of the application first and
      stop their DenoPods only when none is left; the running pds DenoPod removes each record from the repo
      and releases its finalizer. When the pds DenoPod is not running, the core releases the finalizers itself.
    done_when: "dev down of a market network with a bidder and a gateway leaves no AtprotoRecord object and no stuck workspace."
    status: done
  - id: M4-DYNAMIC-ASSOCIATION
    title: "An application learns its operator from the atproto event stream, not from its config"
    depends_on: [M1-BIDDER-OWN-PDS, M2-GATEWAY-REQUESTER-OWN-PDS]
    decision: >
      docs/ARCHITECTURE.md section 14 "The operator comes from the event stream". The kind
      AtprotoRecordWatch (spec {repo, collection, match [{fieldPath, value | valueFrom}], source {relayUrls,
      jetstreamUrls}}, status {records, cursors, conditions Verified}) is reconciled by the pds DenoPod: events
      are triggers, getRecord from the author's PDS verifies, a delete or a failed refetch removes, a start
      re-verifies and resumes from the cursors. deploy/apps/repo-pds/association-watch.<profile>.yaml declares
      association (keyId = MarketIdentity status.did, service = the application's service; source = network
      relayUrls and firehoseUrls, prod adds Jetstream). The MarketIdentity reconciler shows a single-use nonce
      from the application's OpenBao only in status.pendingAction.url; the first verified claim record with
      that nonce (or from the optional spec.operator allow-list) is written to spec.approvedAssociations,
      the nonce is deleted, and the identity is locked. The association AtprotoRecord has
      onMissingReference Delete.
    done_when: "on a market network whose Gateway and MarketBidder specs name no operator, the operators associate by writing one badgeBlueKeys record with the nonce from the MarketIdentity status, the watch lists it as verified, the association AtprotoRecord is Applied, and a tenant cluster gets a node through RFP, bid, accept and cloud-init."
    status: done
  - id: ACC18-NO-OPERATOR-IN-CONFIG
    title: "No operator in the Gateway or MarketBidder spec, in the market network and in prod"
    depends_on: [M4-DYNAMIC-ASSOCIATION]
    done_when: "the prod Gateway and the hat-8 MarketBidder run with no spec.operator; each is associated by one claim record that the operator writes with the nonce of its pendingAction url, read from the public Jetstream."
    status: done
  - id: ACC18-REVOCATION
    title: "Deleting the operator's claim record revokes the association; a restart keeps it by re-verifying"
    depends_on: [M4-DYNAMIC-ASSOCIATION]
    done_when: "deleting the operator's badgeBlueKeys record removes it from the watch status, removes the DID from spec.approvedAssociations, sets condition Associated False reason Revoked, removes the association record from the application's repo and shows a new nonce; a restart of the core with the record in place keeps the association (the watch re-verifies it, Verified True) with no operator in any config."
    status: done