Monorepo for Tangled tangled.org
2

Configure Feed

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

bobbin/containerfiles: switch to glibc

Lewis: May this revision serve well! <lewis@tangled.org>

author
Lewis
date (Jun 17, 2026, 8:43 PM +0300) commit 96a1e447 parent 761e710f change-id txvzpuus
+212 -34
+26
Cargo.lock
··· 303 303 "axum", 304 304 "bobbin-edge-index", 305 305 "bobbin-ingest", 306 + "bobbin-knot-ingest", 306 307 "bobbin-knot-proxy", 307 308 "bobbin-record-lru", 308 309 "bobbin-runtime", ··· 345 346 version = "0.0.1" 346 347 dependencies = [ 347 348 "bobbin-edge-index", 349 + "bobbin-knot-ingest", 348 350 "bobbin-record-lru", 349 351 "bobbin-resolver", 350 352 "bobbin-runtime", ··· 364 366 "tokio-util", 365 367 "tracing", 366 368 "tracing-subscriber", 369 + "url", 370 + "wiremock", 371 + ] 372 + 373 + [[package]] 374 + name = "bobbin-knot-ingest" 375 + version = "0.0.1" 376 + dependencies = [ 377 + "bobbin-edge-index", 378 + "bobbin-knot-proxy", 379 + "bobbin-runtime", 380 + "bobbin-types", 381 + "bytes", 382 + "chrono", 383 + "futures", 384 + "http", 385 + "jacquard-common", 386 + "reqwest", 387 + "serde", 388 + "serde_json", 389 + "thiserror 2.0.18", 390 + "tokio", 391 + "tokio-util", 392 + "tracing", 367 393 "url", 368 394 "wiremock", 369 395 ]
+9 -4
bobbin/containerfiles/bobbin.Containerfile
··· 1 - FROM docker.io/library/rust:1-alpine3.23 AS builder 2 - RUN apk add --no-cache build-base musl-dev cmake perl pkgconfig 1 + FROM rust:1.96-slim-trixie AS builder 2 + RUN apt-get update && apt-get install -y --no-install-recommends \ 3 + ca-certificates pkg-config perl make cmake clang mold \ 4 + && rm -rf /var/lib/apt/lists/* 5 + ENV RUSTFLAGS="-C linker=clang -C link-arg=-fuse-ld=mold" 3 6 ARG BOBBIN_PROFILE=release 4 7 WORKDIR /src 5 8 COPY Cargo.toml Cargo.lock rust-toolchain.toml ./ ··· 8 11 RUN cargo build --profile ${BOBBIN_PROFILE} --bin bobbin --package bobbin 9 12 RUN if [ "${BOBBIN_PROFILE}" = "release" ]; then strip target/${BOBBIN_PROFILE}/bobbin; fi 10 13 11 - FROM docker.io/library/alpine:3.23 14 + FROM debian:trixie-slim 12 15 ARG BOBBIN_PROFILE=release 13 - RUN apk add --no-cache ca-certificates 16 + RUN apt-get update && apt-get install -y --no-install-recommends \ 17 + ca-certificates \ 18 + && rm -rf /var/lib/apt/lists/* 14 19 COPY --from=builder /src/target/${BOBBIN_PROFILE}/bobbin /usr/local/bin/bobbin 15 20 ENV BOBBIN_BIND=0.0.0.0:8090 16 21 EXPOSE 8090
+9 -4
bobbin/containerfiles/hydrant.Containerfile
··· 1 - FROM docker.io/library/rust:1-alpine3.23 AS builder 2 - RUN apk add --no-cache build-base musl-dev cmake perl pkgconfig 1 + FROM rust:1.96-slim-trixie AS builder 2 + RUN apt-get update && apt-get install -y --no-install-recommends \ 3 + ca-certificates pkg-config perl make cmake clang mold \ 4 + && rm -rf /var/lib/apt/lists/* 5 + ENV RUSTFLAGS="-C linker=clang -C link-arg=-fuse-ld=mold" 3 6 WORKDIR /src 4 7 COPY . ./ 5 8 RUN rm -f .cargo/config.toml 6 9 RUN cargo build --release --bin hydrant 7 10 RUN strip target/release/hydrant 8 11 9 - FROM docker.io/library/alpine:3.23 10 - RUN apk add --no-cache ca-certificates 12 + FROM debian:trixie-slim 13 + RUN apt-get update && apt-get install -y --no-install-recommends \ 14 + ca-certificates \ 15 + && rm -rf /var/lib/apt/lists/* 11 16 COPY --from=builder /src/target/release/hydrant /usr/local/bin/hydrant 12 17 ENV HYDRANT_DATABASE_PATH=/var/lib/hydrant 13 18 EXPOSE 3000
+9 -4
bobbin/containerfiles/slingshot.Containerfile
··· 1 - FROM docker.io/library/rust:1-alpine3.23 AS builder 2 - RUN apk add --no-cache build-base musl-dev cmake perl pkgconfig 1 + FROM rust:1.96-slim-trixie AS builder 2 + RUN apt-get update && apt-get install -y --no-install-recommends \ 3 + ca-certificates pkg-config perl make cmake clang mold \ 4 + && rm -rf /var/lib/apt/lists/* 5 + ENV RUSTFLAGS="-C linker=clang -C link-arg=-fuse-ld=mold" 3 6 WORKDIR /src 4 7 COPY . ./ 5 8 RUN rm -f .cargo/config.toml 6 9 RUN cargo build --release --bin slingshot --package slingshot 7 10 RUN strip target/release/slingshot 8 11 9 - FROM docker.io/library/alpine:3.23 10 - RUN apk add --no-cache ca-certificates 12 + FROM debian:trixie-slim 13 + RUN apt-get update && apt-get install -y --no-install-recommends \ 14 + ca-certificates \ 15 + && rm -rf /var/lib/apt/lists/* 11 16 WORKDIR /app 12 17 COPY --from=builder /src/target/release/slingshot /usr/local/bin/slingshot 13 18 COPY --from=builder /src/slingshot/static /app/static
+56 -6
bobbin/crates/types/src/edges.rs
··· 305 305 return primary; 306 306 }; 307 307 let author_subject = SubjectRef::Did(author); 308 + let already: std::collections::HashSet<&str> = 309 + primary.iter().map(|e| e.kind.as_ref()).collect(); 308 310 let mirrors: Vec<Edge> = primary 309 311 .iter() 310 312 .filter_map(|edge| { 311 313 let mirror_nsid = mirror_kind_for(edge.kind.as_ref())?; 314 + if already.contains(mirror_nsid) { 315 + return None; 316 + } 312 317 (edge.subject != author_subject).then(|| Edge { 313 318 kind: nsid_static(mirror_nsid), 314 319 subject: author_subject.clone(), ··· 318 323 }) 319 324 .collect(); 320 325 primary.into_iter().chain(mirrors).collect() 326 + } 327 + 328 + pub(crate) fn subject_keyed_mirror(primary: Edge, mirror_subject: SubjectRef) -> Vec<Edge> { 329 + match mirror_kind_for(primary.kind.as_ref()) { 330 + Some(mirror_nsid) => { 331 + let mirror = Edge { 332 + kind: nsid_static(mirror_nsid), 333 + subject: mirror_subject, 334 + source: primary.source.clone(), 335 + sort_micros: primary.sort_micros, 336 + }; 337 + vec![primary, mirror] 338 + } 339 + None => vec![primary], 340 + } 321 341 } 322 342 323 343 fn star_edges( ··· 414 434 source: &AtUri<DefaultStr>, 415 435 record: &Collaborator<DefaultStr>, 416 436 ) -> Result<Vec<Edge>, ExtractError> { 417 - Ok(one_edge( 418 - "sh.tangled.repo.collaborator", 419 - SubjectRef::Did(record.repo.clone()), 420 - source, 437 + let primary = Edge { 438 + kind: nsid_static("sh.tangled.repo.collaborator"), 439 + subject: SubjectRef::Did(record.repo.clone()), 440 + source: source.clone(), 441 + sort_micros: 0, 442 + }; 443 + Ok(subject_keyed_mirror( 444 + primary, 445 + SubjectRef::Did(record.subject.clone()), 421 446 )) 422 447 } 423 448 ··· 798 823 } 799 824 800 825 #[test] 801 - fn collaborator_keys_on_repo_did() { 826 + fn collaborator_edges_index_repo_and_collaborator() { 802 827 let edges = extract( 803 828 "sh.tangled.repo.collaborator", 804 829 "at://did:plc:nel/sh.tangled.repo.collaborator/abcabcabcabcz", ··· 809 834 "createdAt": "2026-05-01T00:00:00Z" 810 835 }), 811 836 ); 812 - assert_eq!(edges.len(), 1); 837 + assert_eq!(edges.len(), 2); 813 838 assert_eq!(edges[0].kind, nsid("sh.tangled.repo.collaborator")); 814 839 assert_eq!(edges[0].subject, did_subj("did:plc:abalone")); 840 + assert_eq!(edges[1].kind, nsid("sh.tangled.repo.collaborator.by")); 841 + assert_eq!(edges[1].subject, did_subj("did:plc:lyna")); 842 + } 843 + 844 + #[test] 845 + fn collaborator_by_keys_on_collaborator_not_author() { 846 + let parsed = Record::from_json_value( 847 + &nsid("sh.tangled.repo.collaborator"), 848 + json!({ 849 + "$type": "sh.tangled.repo.collaborator", 850 + "repo": "did:plc:abalone", 851 + "subject": "did:plc:lyna", 852 + "createdAt": "2026-05-01T00:00:00Z" 853 + }), 854 + ) 855 + .expect("parse"); 856 + let edges = parsed 857 + .extract_edges(&at("at://did:plc:nel/sh.tangled.repo.collaborator/abcabcabcabcz")) 858 + .expect("extract"); 859 + assert_eq!(edges.len(), 2); 860 + let mirror = edges 861 + .iter() 862 + .find(|e| e.kind == nsid("sh.tangled.repo.collaborator.by")) 863 + .expect("collaborator.by mirror present"); 864 + assert_eq!(mirror.subject, did_subj("did:plc:lyna")); 815 865 } 816 866 817 867 #[test]
+38 -16
bobbin/crates/types/src/knot_acl.rs
··· 103 103 source: source.clone(), 104 104 sort_micros: created_micros, 105 105 }; 106 - Some((source, vec![edge])) 106 + Some(( 107 + source, 108 + crate::edges::subject_keyed_mirror(edge, SubjectRef::Did(knot.clone())), 109 + )) 107 110 } 108 111 109 112 pub fn collaborator_upsert( ··· 118 121 source: source.clone(), 119 122 sort_micros: created_micros, 120 123 }; 121 - Some((source, vec![edge])) 124 + Some(( 125 + source, 126 + crate::edges::subject_keyed_mirror(edge, SubjectRef::Did(subject.clone())), 127 + )) 122 128 } 123 129 124 130 pub fn decode_knot_owned_source(source: &AtUri<DefaultStr>) -> Option<KnotOwnedSource> { ··· 252 258 } 253 259 254 260 #[test] 255 - fn member_upsert_builds_decodable_primary_edge() { 261 + fn member_upsert_builds_primary_and_knot_mirror_edges() { 256 262 let knot = host_to_knot_did("oyster.cafe").unwrap(); 257 263 let subject = did("did:plc:nel"); 258 264 let (source, edges) = 259 265 member_upsert(&knot, &subject, 1_700_000_000_000_000).expect("build member upsert"); 260 - assert_eq!(edges.len(), 1); 261 - let edge = &edges[0]; 262 - assert_eq!(edge.kind.as_ref(), "sh.tangled.knot.member"); 263 - assert_eq!(edge.subject, SubjectRef::Did(subject.clone())); 264 - assert_eq!(edge.source, source); 265 - assert_eq!(edge.sort_micros, 1_700_000_000_000_000); 266 + assert_eq!(edges.len(), 2); 267 + 268 + let primary = &edges[0]; 269 + assert_eq!(primary.kind.as_ref(), "sh.tangled.knot.member"); 270 + assert_eq!(primary.subject, SubjectRef::Did(subject.clone())); 271 + assert_eq!(primary.source, source); 272 + assert_eq!(primary.sort_micros, 1_700_000_000_000_000); 273 + 274 + let mirror = &edges[1]; 275 + assert_eq!(mirror.kind.as_ref(), "sh.tangled.knot.member.by"); 276 + assert_eq!(mirror.subject, SubjectRef::Did(knot.clone())); 277 + assert_eq!(mirror.source, source); 278 + assert_eq!(mirror.sort_micros, 1_700_000_000_000_000); 279 + 266 280 assert_eq!( 267 281 decode_knot_owned_source(&source), 268 282 Some(KnotOwnedSource::Member { knot, subject }) ··· 270 284 } 271 285 272 286 #[test] 273 - fn collaborator_upsert_keys_on_repo_did() { 287 + fn collaborator_upsert_builds_primary_and_subject_mirror_edges() { 274 288 let repo = did("did:plc:scallop"); 275 289 let subject = did("did:plc:olaren"); 276 290 let (source, edges) = 277 291 collaborator_upsert(&repo, &subject, 42).expect("build collaborator upsert"); 278 - assert_eq!(edges.len(), 1); 279 - let edge = &edges[0]; 280 - assert_eq!(edge.kind.as_ref(), "sh.tangled.repo.collaborator"); 281 - assert_eq!(edge.subject, SubjectRef::Did(repo.clone())); 282 - assert_eq!(edge.source, source); 283 - assert_eq!(edge.sort_micros, 42); 292 + assert_eq!(edges.len(), 2); 293 + 294 + let primary = &edges[0]; 295 + assert_eq!(primary.kind.as_ref(), "sh.tangled.repo.collaborator"); 296 + assert_eq!(primary.subject, SubjectRef::Did(repo.clone())); 297 + assert_eq!(primary.source, source); 298 + assert_eq!(primary.sort_micros, 42); 299 + 300 + let mirror = &edges[1]; 301 + assert_eq!(mirror.kind.as_ref(), "sh.tangled.repo.collaborator.by"); 302 + assert_eq!(mirror.subject, SubjectRef::Did(subject.clone())); 303 + assert_eq!(mirror.source, source); 304 + assert_eq!(mirror.sort_micros, 42); 305 + 284 306 assert_eq!( 285 307 decode_knot_owned_source(&source), 286 308 Some(KnotOwnedSource::Collaborator { repo, subject })
+65
bobbin/crates/xrpc/tests/aggregation.rs
··· 2106 2106 } 2107 2107 2108 2108 #[tokio::test] 2109 + async fn knot_owned_member_lists_by_knot_did() { 2110 + let harness = Harness::new().await; 2111 + let knot = bobbin_types::knot_acl::host_to_knot_did("kt.oyster.cafe").unwrap(); 2112 + let subject = did("did:plc:boltless"); 2113 + let created = chrono::DateTime::parse_from_rfc3339("2026-06-01T00:00:00Z").unwrap(); 2114 + let micros = created.timestamp_micros() as u64; 2115 + let (source, edges) = bobbin_types::knot_acl::member_upsert(&knot, &subject, micros).unwrap(); 2116 + harness.edges.upsert_source(&source, edges); 2117 + harness.promote_ready(1, 1); 2118 + 2119 + let (status, body) = json_response( 2120 + router(harness.state.clone()) 2121 + .oneshot(list_request( 2122 + "sh.tangled.knot.listMembersBy", 2123 + knot.as_ref(), 2124 + &[], 2125 + )) 2126 + .await 2127 + .unwrap(), 2128 + ) 2129 + .await; 2130 + 2131 + assert_eq!(status, StatusCode::OK); 2132 + let items = body["items"].as_array().expect("items array"); 2133 + assert_eq!(items.len(), 1); 2134 + assert_eq!(items[0]["uri"], json!(source.as_ref())); 2135 + assert!(items[0]["cid"].is_null()); 2136 + assert_eq!(items[0]["value"]["domain"], json!("kt.oyster.cafe")); 2137 + assert_eq!(items[0]["value"]["subject"], json!("did:plc:boltless")); 2138 + } 2139 + 2140 + #[tokio::test] 2109 2141 async fn knot_owned_collaborator_is_synthesized_without_slingshot() { 2110 2142 let harness = Harness::new().await; 2111 2143 let repo = did("did:plc:scallop"); ··· 2137 2169 assert_eq!(items[0]["value"]["repo"], json!("did:plc:scallop")); 2138 2170 assert_eq!(items[0]["value"]["subject"], json!("did:plc:olaren")); 2139 2171 } 2172 + 2173 + #[tokio::test] 2174 + async fn knot_owned_collaborator_lists_by_subject_did() { 2175 + let harness = Harness::new().await; 2176 + let repo = did("did:plc:scallop"); 2177 + let subject = did("did:plc:olaren"); 2178 + let created = chrono::DateTime::parse_from_rfc3339("2026-06-03T12:00:00Z").unwrap(); 2179 + let micros = created.timestamp_micros() as u64; 2180 + let (source, edges) = 2181 + bobbin_types::knot_acl::collaborator_upsert(&repo, &subject, micros).unwrap(); 2182 + harness.edges.upsert_source(&source, edges); 2183 + harness.promote_ready(1, 1); 2184 + 2185 + let (status, body) = json_response( 2186 + router(harness.state.clone()) 2187 + .oneshot(list_request( 2188 + "sh.tangled.repo.listCollaboratorsBy", 2189 + subject.as_ref(), 2190 + &[], 2191 + )) 2192 + .await 2193 + .unwrap(), 2194 + ) 2195 + .await; 2196 + 2197 + assert_eq!(status, StatusCode::OK); 2198 + let items = body["items"].as_array().expect("items array"); 2199 + assert_eq!(items.len(), 1); 2200 + assert_eq!(items[0]["uri"], json!(source.as_ref())); 2201 + assert!(items[0]["cid"].is_null()); 2202 + assert_eq!(items[0]["value"]["repo"], json!("did:plc:scallop")); 2203 + assert_eq!(items[0]["value"]["subject"], json!("did:plc:olaren")); 2204 + }