···521521braid server --port 5000 \
522522 --public-addr build.example.com \
523523 --key-file /var/lib/braid/server.key \
524524- --cap-file /var/lib/braid/braid.cap \
524524+ --cap-dir /var/lib/braid/caps \
525525+ --users mtelvers,avsm,samoht \
525526 --opam-repo /home/user/opam-repository \
526527 --cache-dir /var/cache/day10
527528```
···530531- `--port PORT` - Port to listen on (required)
531532- `--public-addr HOST` - Public hostname for the capability URI (required)
532533- `--key-file PATH` - Path to store/load the server's secret key (default: server.key)
533533-- `--cap-file PATH` - Path to write the capability file (default: braid.cap)
534534+- `--cap-dir DIR` - Directory to write capability files (default: current directory)
535535+- `--users USER` - User IDs to generate capability files for (comma-separated or multiple flags, required)
534536- `--opam-repo PATH` - Path to opam-repository
535537- `--cache-dir PATH` - Cache directory for day10
536538537539The `--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.
538540539539-### 2. Distribute the Capability File
541541+**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:
542542+- Comma-separated: `--users mtelvers,avsm,samoht`
543543+- Multiple flags: `--users mtelvers --users avsm --users samoht`
540544541541-Copy the capability file to any client machine:
545545+### 2. Distribute the Capability Files
546546+547547+Copy each user's capability file to their client machine:
542548543549```bash
544544-scp build.example.com:/var/lib/braid/braid.cap ~/.config/braid.cap
550550+scp build.example.com:/var/lib/braid/caps/mtelvers.cap ~/.config/braid.cap
545551```
546552547547-The capability file contains a URI like:
553553+Each capability file contains a URI like:
548554```
549555capnp://sha-256:abc123...@build.example.com:5000/def456...
550556```
551557552552-This URI encodes both the server address and a cryptographic capability token.
558558+This URI encodes both the server address and a cryptographic capability token unique to that user.
559559+560560+**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.
553561554562### 3. Run Remote Merge Tests
555563···619627braid server --port 5000 \
620628 --public-addr basil.caelum.ci.dev \
621629 --key-file ~/braid-server.key \
622622- --cap-file ~/braid.cap \
630630+ --cap-dir ~/caps \
631631+ --users mtelvers,avsm \
623632 --opam-repo ~/opam-repository \
624633 --cache-dir /var/cache/day10
625634626626-# Distribute capability (once)
627627-scp basil.caelum.ci.dev:~/braid.cap ~/.config/
635635+# Distribute capabilities (once per user)
636636+scp basil.caelum.ci.dev:~/caps/mtelvers.cap ~/.config/braid.cap
628637629638# From any client - test an overlay
630639braid merge-test https://github.com/mtelvers/claude-repo \
+19-6
bin/main.ml
···224224 let doc = "Path to secret key file (created if doesn't exist)" in
225225 Arg.(value & opt string "braid.key" & info ["key-file"] ~docv:"FILE" ~doc)
226226 in
227227- let cap_file_arg =
228228- let doc = "Path to write capability file" in
229229- Arg.(value & opt string "braid.cap" & info ["cap-file"] ~docv:"FILE" ~doc)
227227+ let cap_dir_arg =
228228+ let doc = "Directory to write capability files (one per user)" in
229229+ Arg.(value & opt string "." & info ["cap-dir"] ~docv:"DIR" ~doc)
230230+ in
231231+ let users_arg =
232232+ let doc = "User IDs to generate capability files for (comma-separated or multiple flags)" in
233233+ Arg.(non_empty & opt_all string [] & info ["users"] ~docv:"USER" ~doc)
230234 in
231235 let listen_addr_arg =
232236 let doc = "Address to listen on" in
···241245 Arg.(value & opt string "/var/cache/day10" & info ["cache-dir"] ~docv:"PATH" ~doc)
242246 in
243247244244- let server _setup port public_addr key_file cap_file listen_addr opam_repo cache_dir =
248248+ let server _setup port public_addr key_file cap_dir users listen_addr opam_repo cache_dir =
249249+ (* Expand comma-separated user lists into individual users *)
250250+ let users = List.concat_map (String.split_on_char ',') users
251251+ |> List.map String.trim
252252+ |> List.filter (fun s -> String.length s > 0)
253253+ in
254254+ if users = [] then begin
255255+ Fmt.epr "Error: at least one user must be specified with --users@.";
256256+ exit 1
257257+ end;
245258 Eio_main.run @@ fun env ->
246259 Eio.Switch.run @@ fun sw ->
247260 let net = Eio.Stdenv.net env in
248261 let fs = Eio.Stdenv.cwd env in
249262 Server.run ~sw ~net ~fs ~listen_addr ~listen_port:port ~public_addr
250250- ~key_file ~cap_file ~opam_repo_path:opam_repo ~cache_dir
263263+ ~key_file ~cap_dir ~users ~opam_repo_path:opam_repo ~cache_dir
251264 in
252265253266 let doc = "Start RPC server for remote braid execution" in
254267 let info = Cmd.info "server" ~doc in
255268 Cmd.v info Term.(const server $ setup_log_term $ port_arg $ public_addr_arg
256256- $ key_file_arg $ cap_file_arg $ listen_addr_arg $ opam_repo $ cache_dir)
269269+ $ key_file_arg $ cap_dir_arg $ users_arg $ listen_addr_arg $ opam_repo $ cache_dir)
257270258271(* Query: failures *)
259272let failures_cmd =
+27-6
lib/server.ml
···11(** Cap'n Proto RPC server for BraidService *)
2233-(** Start the RPC server *)
44-let run ~sw ~net ~fs ~listen_addr ~listen_port ~public_addr ~key_file ~cap_file ~opam_repo_path ~cache_dir =
33+(** Start the RPC server with per-user capability files *)
44+let run ~sw ~net ~fs ~listen_addr ~listen_port ~public_addr ~key_file ~cap_dir ~users ~opam_repo_path ~cache_dir =
55 let service = Rpc_service.local ~opam_repo_path ~cache_dir in
66 let addr = `TCP (listen_addr, listen_port) in
77 let public_address = `TCP (public_addr, listen_port) in
88 let secret_key = `File (Eio.Path.(fs / key_file)) in
99 let config = Capnp_rpc_unix.Vat_config.create ~secret_key ~public_address ~net addr in
1010- let service_id = Capnp_rpc_unix.Vat_config.derived_id config "main" in
1111- let restore = Capnp_rpc_net.Restorer.single service_id service in
1010+1111+ (* Create a restorer table with an entry for each user *)
1212+ let make_sturdy = Capnp_rpc_unix.Vat_config.sturdy_uri config in
1313+ let table = Capnp_rpc_net.Restorer.Table.create make_sturdy ~sw in
1414+1515+ (* Generate a derived ID for each user and add to the table *)
1616+ let user_ids = List.map (fun user ->
1717+ let service_id = Capnp_rpc_unix.Vat_config.derived_id config user in
1818+ Capnp_rpc_net.Restorer.Table.add table service_id service;
1919+ (user, service_id)
2020+ ) users in
2121+2222+ let restore = Capnp_rpc_net.Restorer.of_table table in
1223 let vat = Capnp_rpc_unix.serve ~sw ~restore config in
1313- Capnp_rpc_unix.Cap_file.save_service vat service_id cap_file |> Result.get_ok;
2424+2525+ (* Create cap_dir if it doesn't exist *)
2626+ (try Unix.mkdir cap_dir 0o755 with Unix.Unix_error (Unix.EEXIST, _, _) -> ());
2727+2828+ (* Save a capability file for each user *)
2929+ List.iter (fun (user, service_id) ->
3030+ let cap_file = Filename.concat cap_dir (user ^ ".cap") in
3131+ Capnp_rpc_unix.Cap_file.save_service vat service_id cap_file |> Result.get_ok;
3232+ Fmt.pr " Created capability file: %s@." cap_file
3333+ ) user_ids;
3434+1435 Fmt.pr "Server listening on %s:%d@." listen_addr listen_port;
1536 Fmt.pr " Public address: %s:%d@." public_addr listen_port;
1637 Fmt.pr " Key file: %s@." key_file;
1717- Fmt.pr " Capability file: %s@." cap_file;
3838+ Fmt.pr " Users: %s@." (String.concat ", " users);
1839 (* Block forever - server runs until killed *)
1940 Eio.Fiber.await_cancel ()