Pure OCaml xxhash implementation

Squashed 'braid/' changes from 20ed263a..d7db8df9

d7db8df9 Add multi-user capability support for RPC server

git-subtree-dir: braid
git-subtree-split: d7db8df98245c093c90a5227815e2678187840eb

+65 -22
+19 -10
README.md
··· 521 521 braid server --port 5000 \ 522 522 --public-addr build.example.com \ 523 523 --key-file /var/lib/braid/server.key \ 524 - --cap-file /var/lib/braid/braid.cap \ 524 + --cap-dir /var/lib/braid/caps \ 525 + --users mtelvers,avsm,samoht \ 525 526 --opam-repo /home/user/opam-repository \ 526 527 --cache-dir /var/cache/day10 527 528 ``` ··· 530 531 - `--port PORT` - Port to listen on (required) 531 532 - `--public-addr HOST` - Public hostname for the capability URI (required) 532 533 - `--key-file PATH` - Path to store/load the server's secret key (default: server.key) 533 - - `--cap-file PATH` - Path to write the capability file (default: braid.cap) 534 + - `--cap-dir DIR` - Directory to write capability files (default: current directory) 535 + - `--users USER` - User IDs to generate capability files for (comma-separated or multiple flags, required) 534 536 - `--opam-repo PATH` - Path to opam-repository 535 537 - `--cache-dir PATH` - Cache directory for day10 536 538 537 539 The `--key-file` option ensures the capability URI remains stable across server restarts. Without it, clients would need a new capability file each time the server restarts. 538 540 539 - ### 2. Distribute the Capability File 541 + **Multi-user support:** The server generates a separate capability file for each user (e.g., `mtelvers.cap`, `avsm.cap`, `samoht.cap`). This allows individual users to be revoked by simply removing them from the `--users` list and restarting the server. Users can specify IDs as: 542 + - Comma-separated: `--users mtelvers,avsm,samoht` 543 + - Multiple flags: `--users mtelvers --users avsm --users samoht` 540 544 541 - Copy the capability file to any client machine: 545 + ### 2. Distribute the Capability Files 546 + 547 + Copy each user's capability file to their client machine: 542 548 543 549 ```bash 544 - scp build.example.com:/var/lib/braid/braid.cap ~/.config/braid.cap 550 + scp build.example.com:/var/lib/braid/caps/mtelvers.cap ~/.config/braid.cap 545 551 ``` 546 552 547 - The capability file contains a URI like: 553 + Each capability file contains a URI like: 548 554 ``` 549 555 capnp://sha-256:abc123...@build.example.com:5000/def456... 550 556 ``` 551 557 552 - This URI encodes both the server address and a cryptographic capability token. 558 + This URI encodes both the server address and a cryptographic capability token unique to that user. 559 + 560 + **Revoking access:** To revoke a user's access, simply restart the server without that user in the `--users` list. Their capability file will no longer work. 553 561 554 562 ### 3. Run Remote Merge Tests 555 563 ··· 619 627 braid server --port 5000 \ 620 628 --public-addr basil.caelum.ci.dev \ 621 629 --key-file ~/braid-server.key \ 622 - --cap-file ~/braid.cap \ 630 + --cap-dir ~/caps \ 631 + --users mtelvers,avsm \ 623 632 --opam-repo ~/opam-repository \ 624 633 --cache-dir /var/cache/day10 625 634 626 - # Distribute capability (once) 627 - scp basil.caelum.ci.dev:~/braid.cap ~/.config/ 635 + # Distribute capabilities (once per user) 636 + scp basil.caelum.ci.dev:~/caps/mtelvers.cap ~/.config/braid.cap 628 637 629 638 # From any client - test an overlay 630 639 braid merge-test https://github.com/mtelvers/claude-repo \
+19 -6
bin/main.ml
··· 224 224 let doc = "Path to secret key file (created if doesn't exist)" in 225 225 Arg.(value & opt string "braid.key" & info ["key-file"] ~docv:"FILE" ~doc) 226 226 in 227 - let cap_file_arg = 228 - let doc = "Path to write capability file" in 229 - Arg.(value & opt string "braid.cap" & info ["cap-file"] ~docv:"FILE" ~doc) 227 + let cap_dir_arg = 228 + let doc = "Directory to write capability files (one per user)" in 229 + Arg.(value & opt string "." & info ["cap-dir"] ~docv:"DIR" ~doc) 230 + in 231 + let users_arg = 232 + let doc = "User IDs to generate capability files for (comma-separated or multiple flags)" in 233 + Arg.(non_empty & opt_all string [] & info ["users"] ~docv:"USER" ~doc) 230 234 in 231 235 let listen_addr_arg = 232 236 let doc = "Address to listen on" in ··· 241 245 Arg.(value & opt string "/var/cache/day10" & info ["cache-dir"] ~docv:"PATH" ~doc) 242 246 in 243 247 244 - let server _setup port public_addr key_file cap_file listen_addr opam_repo cache_dir = 248 + let server _setup port public_addr key_file cap_dir users listen_addr opam_repo cache_dir = 249 + (* Expand comma-separated user lists into individual users *) 250 + let users = List.concat_map (String.split_on_char ',') users 251 + |> List.map String.trim 252 + |> List.filter (fun s -> String.length s > 0) 253 + in 254 + if users = [] then begin 255 + Fmt.epr "Error: at least one user must be specified with --users@."; 256 + exit 1 257 + end; 245 258 Eio_main.run @@ fun env -> 246 259 Eio.Switch.run @@ fun sw -> 247 260 let net = Eio.Stdenv.net env in 248 261 let fs = Eio.Stdenv.cwd env in 249 262 Server.run ~sw ~net ~fs ~listen_addr ~listen_port:port ~public_addr 250 - ~key_file ~cap_file ~opam_repo_path:opam_repo ~cache_dir 263 + ~key_file ~cap_dir ~users ~opam_repo_path:opam_repo ~cache_dir 251 264 in 252 265 253 266 let doc = "Start RPC server for remote braid execution" in 254 267 let info = Cmd.info "server" ~doc in 255 268 Cmd.v info Term.(const server $ setup_log_term $ port_arg $ public_addr_arg 256 - $ key_file_arg $ cap_file_arg $ listen_addr_arg $ opam_repo $ cache_dir) 269 + $ key_file_arg $ cap_dir_arg $ users_arg $ listen_addr_arg $ opam_repo $ cache_dir) 257 270 258 271 (* Query: failures *) 259 272 let failures_cmd =
+27 -6
lib/server.ml
··· 1 1 (** Cap'n Proto RPC server for BraidService *) 2 2 3 - (** Start the RPC server *) 4 - let run ~sw ~net ~fs ~listen_addr ~listen_port ~public_addr ~key_file ~cap_file ~opam_repo_path ~cache_dir = 3 + (** Start the RPC server with per-user capability files *) 4 + let run ~sw ~net ~fs ~listen_addr ~listen_port ~public_addr ~key_file ~cap_dir ~users ~opam_repo_path ~cache_dir = 5 5 let service = Rpc_service.local ~opam_repo_path ~cache_dir in 6 6 let addr = `TCP (listen_addr, listen_port) in 7 7 let public_address = `TCP (public_addr, listen_port) in 8 8 let secret_key = `File (Eio.Path.(fs / key_file)) in 9 9 let config = Capnp_rpc_unix.Vat_config.create ~secret_key ~public_address ~net addr in 10 - let service_id = Capnp_rpc_unix.Vat_config.derived_id config "main" in 11 - let restore = Capnp_rpc_net.Restorer.single service_id service in 10 + 11 + (* Create a restorer table with an entry for each user *) 12 + let make_sturdy = Capnp_rpc_unix.Vat_config.sturdy_uri config in 13 + let table = Capnp_rpc_net.Restorer.Table.create make_sturdy ~sw in 14 + 15 + (* Generate a derived ID for each user and add to the table *) 16 + let user_ids = List.map (fun user -> 17 + let service_id = Capnp_rpc_unix.Vat_config.derived_id config user in 18 + Capnp_rpc_net.Restorer.Table.add table service_id service; 19 + (user, service_id) 20 + ) users in 21 + 22 + let restore = Capnp_rpc_net.Restorer.of_table table in 12 23 let vat = Capnp_rpc_unix.serve ~sw ~restore config in 13 - Capnp_rpc_unix.Cap_file.save_service vat service_id cap_file |> Result.get_ok; 24 + 25 + (* Create cap_dir if it doesn't exist *) 26 + (try Unix.mkdir cap_dir 0o755 with Unix.Unix_error (Unix.EEXIST, _, _) -> ()); 27 + 28 + (* Save a capability file for each user *) 29 + List.iter (fun (user, service_id) -> 30 + let cap_file = Filename.concat cap_dir (user ^ ".cap") in 31 + Capnp_rpc_unix.Cap_file.save_service vat service_id cap_file |> Result.get_ok; 32 + Fmt.pr " Created capability file: %s@." cap_file 33 + ) user_ids; 34 + 14 35 Fmt.pr "Server listening on %s:%d@." listen_addr listen_port; 15 36 Fmt.pr " Public address: %s:%d@." public_addr listen_port; 16 37 Fmt.pr " Key file: %s@." key_file; 17 - Fmt.pr " Capability file: %s@." cap_file; 38 + Fmt.pr " Users: %s@." (String.concat ", " users); 18 39 (* Block forever - server runs until killed *) 19 40 Eio.Fiber.await_cancel ()