This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

spindle: remove spindle owner

Signed-off-by: Seongmin Lee <git@boltless.me>

author
Seongmin Lee
date (Jul 29, 2026, 3:43 PM +0900) commit f0d8cd9e parent 759b0633 change-id lrtszopx
+4 -301
-1
contrib/Tiltfile
··· 1 1 common_env = { 2 - "TANGLED_VM_SPINDLE_OWNER": os.getenv("TANGLED_VM_SPINDLE_OWNER", default=""), 3 2 "TANGLED_VM_KNOT_OWNER": os.getenv("TANGLED_VM_KNOT_OWNER", default=""), 4 3 "TANGLED_DB_PATH": os.getenv("TANGLED_DB_PATH", default="dev.db"), 5 4 "TANGLED_DEV": os.getenv("TANGLED_DEV", default="true"),
+1 -4
docs/DOCS.md
··· 1453 1453 - `SPINDLE_SERVER_HOSTNAME`: The hostname of the server (required). 1454 1454 - `SPINDLE_SERVER_JETSTREAM_ENDPOINT`: The endpoint of the Jetstream server (default: `"wss://jetstream1.us-west.bsky.network/subscribe"`). 1455 1455 - `SPINDLE_SERVER_DEV`: A boolean indicating whether the server is running in development mode (default: `false`). 1456 - - `SPINDLE_SERVER_OWNER`: The DID of the owner (required). 1457 1456 - `SPINDLE_SERVER_LOG_DIR`: The directory to store workflow logs (default: `"/var/log/spindle"`). 1458 1457 - `SPINDLE_SERVER_DOCKER_SOCKET`: Path to Docker socket to expose to invoked Spindle containers (default: `""`). 1459 1458 - `SPINDLE_NIXERY_PIPELINES_NIXERY`: The Nixery URL (default: `"nixery.tangled.sh"`). ··· 1516 1515 1517 1516 ```shell 1518 1517 export SPINDLE_SERVER_HOSTNAME="your-hostname" 1519 - export SPINDLE_SERVER_OWNER="your-did" 1520 1518 ``` 1521 1519 1522 1520 2. **Build the Spindle binary.** ··· 2637 2635 </details> 2638 2636 2639 2637 To begin, grab your DID from http://localhost:3000/settings. 2640 - Then, set `TANGLED_VM_KNOT_OWNER` and 2641 - `TANGLED_VM_SPINDLE_OWNER` to your DID. You can now start a 2638 + Then, set `TANGLED_VM_KNOT_OWNER` to your DID. You can now start a 2642 2639 lightweight NixOS VM like so: 2643 2640 2644 2641 ```bash
-4
localinfra/spindle.Dockerfile
··· 45 45 #!/bin/sh 46 46 set -eu 47 47 48 - [ -z "${SPINDLE_SERVER_OWNER:-}" ] && [ -r /shared/owner-did ] && \ 49 - export SPINDLE_SERVER_OWNER="$(cat /shared/owner-did)" 50 - : "${SPINDLE_SERVER_OWNER:?set via env or /shared/owner-did}" 51 - 52 48 mkdir -p /var/lib/spindle /var/lib/spindle/overlays /var/log/spindle 53 49 54 50 if [ -f /usr/local/share/ca-certificates/caddy.crt ]; then
+3 -7
nix/modules/spindle.nix
··· 7 7 cfg = config.services.tangled.spindle; 8 8 in 9 9 with lib; { 10 + imports = [ 11 + (lib.mkRemovedOptionModule [ "services" "tangled" "spindle" "server" "owner" ] "The spindle owner setting is deprecated.") 12 + ]; 10 13 options = { 11 14 services.tangled.spindle = { 12 15 enable = mkOption { ··· 60 63 type = types.bool; 61 64 default = false; 62 65 description = "Enable development mode (disables signature verification)"; 63 - }; 64 - 65 - owner = mkOption { 66 - type = types.str; 67 - example = "did:plc:qfpnj4og54vl56wngdriaxug"; 68 - description = "DID of owner (required)"; 69 66 }; 70 67 71 68 maxJobCount = mkOption { ··· 353 350 "SPINDLE_SERVER_PLC_URL=${cfg.server.plcUrl}" 354 351 "SPINDLE_SERVER_JETSTREAM_ENDPOINT=${cfg.server.jetstreamEndpoint}" 355 352 "SPINDLE_SERVER_DEV=${lib.boolToString cfg.server.dev}" 356 - "SPINDLE_SERVER_OWNER=${cfg.server.owner}" 357 353 "SPINDLE_SERVER_MAX_JOB_COUNT=${toString cfg.server.maxJobCount}" 358 354 "SPINDLE_SERVER_QUEUE_SIZE=${toString cfg.server.queueSize}" 359 355 "SPINDLE_SERVER_SECRETS_PROVIDER=${cfg.server.secrets.provider}"
-1
nix/vm.nix
··· 149 149 services.tangled.spindle = { 150 150 enable = true; 151 151 server = { 152 - owner = envVar "TANGLED_VM_SPINDLE_OWNER"; 153 152 hostname = envVarOr "TANGLED_VM_SPINDLE_HOST" "localhost:6555"; 154 153 plcUrl = plcUrl; 155 154 jetstreamEndpoint = jetstream;
-1
spindle/config/config.go
··· 19 19 PlcUrl string `env:"PLC_URL, default=https://plc.directory"` 20 20 Dev bool `env:"DEV, default=false"` 21 21 DevExtraHosts []string `env:"DEV_EXTRA_HOSTS"` 22 - Owner string `env:"OWNER, required"` 23 22 Secrets Secrets `env:",prefix=SECRETS_"` 24 23 LogDir string `env:"LOG_DIR, default=/var/log/spindle"` 25 24 QueueSize int `env:"QUEUE_SIZE, default=100"`
-1
spindle/config/config_test.go
··· 7 7 8 8 func TestLoadAllowsUnconfiguredMicroVMEngine(t *testing.T) { 9 9 t.Setenv("SPINDLE_SERVER_HOSTNAME", "spindle.example.com") 10 - t.Setenv("SPINDLE_SERVER_OWNER", "did:web:spindle.example.com") 11 10 t.Setenv("SPINDLE_MICROVM_PIPELINES_IMAGE_DIR", "") 12 11 13 12 cfg, err := Load(context.Background())
-206
spindle/ingester_test.go
··· 3 3 import ( 4 4 "context" 5 5 "encoding/json" 6 - "log/slog" 7 - "strings" 8 - "tangled.org/core/jetstream" 9 6 "testing" 10 7 11 8 "github.com/bluesky-social/indigo/atproto/syntax" 12 9 "github.com/bluesky-social/jetstream/pkg/models" 13 10 14 11 "tangled.org/core/api/tangled" 15 - "tangled.org/core/rbac" 16 12 "tangled.org/core/spindle/config" 17 13 "tangled.org/core/tapc" 18 14 ) ··· 89 85 } 90 86 } 91 87 } 92 - 93 - func TestIngestMember_RBAC(t *testing.T) { 94 - d, e := newTestSpindleDB(t) 95 - 96 - cfg := &config.Config{} 97 - cfg.Server.Hostname = "spindle.test" 98 - 99 - jc, jcerr := jetstream.NewJetstreamClient("", "", nil, nil, slog.Default(), nil, false, false) 100 - if jcerr != nil { 101 - t.Fatalf("NewJetstreamClient: %v", jcerr) 102 - } 103 - 104 - s := &Spindle{ 105 - db: d, 106 - e: e, 107 - l: slog.Default(), 108 - cfg: cfg, 109 - jc: jc, 110 - rootCtx: context.Background(), 111 - } 112 - 113 - actorDid := "did:plc:adminactor" 114 - subjectDid := "did:plc:newmember" 115 - rbacDomain := rbac.ThisServer 116 - 117 - memberRecord := tangled.SpindleMember{ 118 - Instance: "spindle.test", 119 - Subject: subjectDid, 120 - } 121 - memberRecordJson, _ := json.Marshal(memberRecord) 122 - 123 - evt := &models.Event{ 124 - Did: actorDid, 125 - Kind: models.EventKindCommit, 126 - Commit: &models.Commit{ 127 - Operation: models.CommitOperationCreate, 128 - Collection: tangled.SpindleMemberNSID, 129 - RKey: "member-rkey-1", 130 - Record: memberRecordJson, 131 - }, 132 - } 133 - 134 - err := s.ingestMember(context.Background(), evt) 135 - if err == nil { 136 - t.Fatal("expected permission denied error, got nil") 137 - } 138 - if !strings.Contains(err.Error(), "permission denied") { 139 - t.Fatalf("expected permission denied, got error: %v", err) 140 - } 141 - 142 - var dbCount int 143 - err = d.QueryRow(`select count(*) from spindle_members where subject = ?`, subjectDid).Scan(&dbCount) 144 - if err != nil { 145 - t.Fatalf("DB query error: %v", err) 146 - } 147 - if dbCount > 0 { 148 - t.Fatal("spindle member was registered in DB on failed auth") 149 - } 150 - 151 - err = e.AddSpindle(rbacDomain) 152 - if err != nil { 153 - t.Fatalf("AddSpindle: %v", err) 154 - } 155 - err = e.AddSpindleOwner(rbacDomain, actorDid) 156 - if err != nil { 157 - t.Fatalf("AddSpindleOwner: %v", err) 158 - } 159 - 160 - err = s.ingestMember(context.Background(), evt) 161 - if err != nil { 162 - t.Fatalf("ingestMember failed for authorized actor: %v", err) 163 - } 164 - 165 - err = d.QueryRow(`select count(*) from spindle_members where subject = ?`, subjectDid).Scan(&dbCount) 166 - if err != nil || dbCount != 1 { 167 - t.Fatalf("expected exactly 1 member in DB, got: %d (err: %v)", dbCount, err) 168 - } 169 - 170 - isMember, err := e.IsSpindleMember(subjectDid, rbacDomain) 171 - if err != nil || !isMember { 172 - t.Fatalf("expected subject to be spindle member in Casbin, got: %t (err: %v)", isMember, err) 173 - } 174 - 175 - deleteEvt := &models.Event{ 176 - Did: actorDid, 177 - Kind: models.EventKindCommit, 178 - Commit: &models.Commit{ 179 - Operation: models.CommitOperationDelete, 180 - Collection: tangled.SpindleMemberNSID, 181 - RKey: "member-rkey-1", 182 - }, 183 - } 184 - 185 - err = s.ingestMember(context.Background(), deleteEvt) 186 - if err != nil { 187 - t.Fatalf("ingestMember delete failed: %v", err) 188 - } 189 - 190 - err = d.QueryRow(`select count(*) from spindle_members where subject = ?`, subjectDid).Scan(&dbCount) 191 - if err != nil || dbCount != 0 { 192 - t.Fatalf("expected 0 members in DB after delete, got: %d (err: %v)", dbCount, err) 193 - } 194 - 195 - isMember, err = e.IsSpindleMember(subjectDid, rbacDomain) 196 - if err != nil || isMember { 197 - t.Fatalf("expected subject to NOT be spindle member in Casbin, got: %t (err: %v)", isMember, err) 198 - } 199 - } 200 - 201 - func TestIngestMember_ForgeDeleteRejection(t *testing.T) { 202 - d, e := newTestSpindleDB(t) 203 - 204 - cfg := &config.Config{} 205 - cfg.Server.Hostname = "spindle.test" 206 - 207 - jc, jcerr := jetstream.NewJetstreamClient("", "", nil, nil, slog.Default(), nil, false, false) 208 - if jcerr != nil { 209 - t.Fatalf("NewJetstreamClient: %v", jcerr) 210 - } 211 - 212 - s := &Spindle{ 213 - db: d, 214 - e: e, 215 - l: slog.Default(), 216 - cfg: cfg, 217 - jc: jc, 218 - rootCtx: context.Background(), 219 - } 220 - 221 - adminDid := "did:plc:adminactor" 222 - bobDid := "did:plc:bobactor" 223 - subjectDid := "did:plc:newmember" 224 - rbacDomain := rbac.ThisServer 225 - 226 - err := e.AddSpindle(rbacDomain) 227 - if err != nil { 228 - t.Fatalf("AddSpindle: %v", err) 229 - } 230 - err = e.AddSpindleOwner(rbacDomain, adminDid) 231 - if err != nil { 232 - t.Fatalf("AddSpindleOwner: %v", err) 233 - } 234 - 235 - memberRecord := tangled.SpindleMember{ 236 - Instance: "spindle.test", 237 - Subject: subjectDid, 238 - } 239 - memberRecordJson, _ := json.Marshal(memberRecord) 240 - 241 - evt := &models.Event{ 242 - Did: adminDid, 243 - Kind: models.EventKindCommit, 244 - Commit: &models.Commit{ 245 - Operation: models.CommitOperationCreate, 246 - Collection: tangled.SpindleMemberNSID, 247 - RKey: "member-rkey-1", 248 - Record: memberRecordJson, 249 - }, 250 - } 251 - 252 - err = s.ingestMember(context.Background(), evt) 253 - if err != nil { 254 - t.Fatalf("ingestMember failed for admin: %v", err) 255 - } 256 - 257 - var dbCount int 258 - err = d.QueryRow(`select count(*) from spindle_members where subject = ?`, subjectDid).Scan(&dbCount) 259 - if err != nil || dbCount != 1 { 260 - t.Fatalf("expected member in DB, got: %d (err: %v)", dbCount, err) 261 - } 262 - 263 - isMember, err := e.IsSpindleMember(subjectDid, rbacDomain) 264 - if err != nil || !isMember { 265 - t.Fatalf("expected subject to be spindle member, got %t (err: %v)", isMember, err) 266 - } 267 - 268 - // bob tries to delete alice's spindle member record, must reject forged delete 269 - deleteEvt := &models.Event{ 270 - Did: bobDid, // Bob is the actor 271 - Kind: models.EventKindCommit, 272 - Commit: &models.Commit{ 273 - Operation: models.CommitOperationDelete, 274 - Collection: tangled.SpindleMemberNSID, 275 - RKey: "member-rkey-1", 276 - }, 277 - } 278 - 279 - err = s.ingestMember(context.Background(), deleteEvt) 280 - if err != nil { 281 - t.Fatalf("ingestMember delete returned error: %v", err) 282 - } 283 - 284 - err = d.QueryRow(`select count(*) from spindle_members where subject = ?`, subjectDid).Scan(&dbCount) 285 - if err != nil || dbCount != 1 { 286 - t.Fatalf("member was deleted from DB, expected remaining, count: %d (err: %v)", dbCount, err) 287 - } 288 - 289 - isMember, err = e.IsSpindleMember(subjectDid, rbacDomain) 290 - if err != nil || !isMember { 291 - t.Fatal("member policy was removed from Casbin by forged delete") 292 - } 293 - }
-44
spindle/server.go
··· 113 113 } 114 114 115 115 collections := []string{ 116 - tangled.SpindleMemberNSID, 117 116 tangled.RepoNSID, 118 117 tangled.RepoCollaboratorNSID, 119 118 tangled.RepoPullNSID, ··· 122 121 if err != nil { 123 122 return nil, fmt.Errorf("failed to setup jetstream client: %w", err) 124 123 } 125 - jc.AddDid(cfg.Server.Owner) 126 124 // pull records are created by arbitrary users too, same hack as in tap 127 125 jc.ExemptCollection(tangled.RepoPullNSID) 128 126 ··· 162 160 rootCtx: ctx, 163 161 jobWake: make(chan struct{}, 1), 164 162 } 165 - 166 - err = e.AddSpindle(rbacDomain) 167 - if err != nil { 168 - return nil, fmt.Errorf("failed to set rbac domain: %w", err) 169 - } 170 - err = spindle.configureOwner() 171 - if err != nil { 172 - return nil, err 173 - } 174 - logger.Info("owner set", "did", cfg.Server.Owner) 175 163 176 164 cursorStore, err := cursor.NewSQLiteStore(cfg.Server.DBPath) 177 165 if err != nil { ··· 885 873 } 886 874 return repoDid, nil 887 875 } 888 - 889 - func (s *Spindle) configureOwner() error { 890 - cfgOwner := s.cfg.Server.Owner 891 - 892 - existing, err := s.e.GetSpindleUsersByRole("server:owner", rbacDomain) 893 - if err != nil { 894 - return err 895 - } 896 - 897 - switch len(existing) { 898 - case 0: 899 - // no owner configured, continue 900 - case 1: 901 - // find existing owner 902 - existingOwner := existing[0] 903 - 904 - // no ownership change, this is okay 905 - if existingOwner == s.cfg.Server.Owner { 906 - break 907 - } 908 - 909 - // remove existing owner 910 - err = s.e.RemoveSpindleOwner(rbacDomain, existingOwner) 911 - if err != nil { 912 - return nil 913 - } 914 - default: 915 - return fmt.Errorf("more than one owner in DB, try deleting %q and starting over", s.cfg.Server.DBPath) 916 - } 917 - 918 - return s.e.AddSpindleOwner(rbacDomain, cfgOwner) 919 - }
-31
spindle/xrpc/owner.go
··· 1 - package xrpc 2 - 3 - import ( 4 - "encoding/json" 5 - "net/http" 6 - 7 - "tangled.org/core/api/tangled" 8 - xrpcerr "tangled.org/core/xrpc/errors" 9 - ) 10 - 11 - func (x *Xrpc) Owner(w http.ResponseWriter, r *http.Request) { 12 - owner := x.Config.Server.Owner 13 - if owner == "" { 14 - writeError(w, xrpcerr.OwnerNotFoundError, http.StatusInternalServerError) 15 - return 16 - } 17 - 18 - response := tangled.Owner_Output{ 19 - Owner: owner, 20 - } 21 - 22 - w.Header().Set("Content-Type", "application/json") 23 - if err := json.NewEncoder(w).Encode(response); err != nil { 24 - x.Logger.Error("failed to encode response", "error", err) 25 - writeError(w, xrpcerr.NewXrpcError( 26 - xrpcerr.WithTag("InternalServerError"), 27 - xrpcerr.WithMessage("failed to encode response"), 28 - ), http.StatusInternalServerError) 29 - return 30 - } 31 - }
-1
spindle/xrpc/xrpc.go
··· 67 67 }) 68 68 69 69 // service query endpoints (no auth required) 70 - r.Get("/"+tangled.OwnerNSID, x.Owner) 71 70 r.Get("/"+tangled.CiSubscribePipelineLogsNSID, x.HandleCiSubscribePipelineLogs) 72 71 r.Get("/"+tangled.CiQueryPipelinesNSID, x.HandleCiQueryPipelines) 73 72 r.Get("/"+tangled.CiGetPipelineNSID, x.HandleCiGetPipeline)