···521braid server --port 5000 \
522 --public-addr build.example.com \
523 --key-file /var/lib/braid/server.key \
524- --cap-file /var/lib/braid/braid.cap \
0525 --opam-repo /home/user/opam-repository \
526 --cache-dir /var/cache/day10
527```
···530- `--port PORT` - Port to listen on (required)
531- `--public-addr HOST` - Public hostname for the capability URI (required)
532- `--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)
0534- `--opam-repo PATH` - Path to opam-repository
535- `--cache-dir PATH` - Cache directory for day10
536537The `--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.
538539-### 2. Distribute the Capability File
00540541-Copy the capability file to any client machine:
00542543```bash
544-scp build.example.com:/var/lib/braid/braid.cap ~/.config/braid.cap
545```
546547-The capability file contains a URI like:
548```
549capnp://sha-256:abc123...@build.example.com:5000/def456...
550```
551552-This URI encodes both the server address and a cryptographic capability token.
00553554### 3. Run Remote Merge Tests
555···619braid server --port 5000 \
620 --public-addr basil.caelum.ci.dev \
621 --key-file ~/braid-server.key \
622- --cap-file ~/braid.cap \
0623 --opam-repo ~/opam-repository \
624 --cache-dir /var/cache/day10
625626-# Distribute capability (once)
627-scp basil.caelum.ci.dev:~/braid.cap ~/.config/
628629# From any client - test an overlay
630braid merge-test https://github.com/mtelvers/claude-repo \
···521braid server --port 5000 \
522 --public-addr build.example.com \
523 --key-file /var/lib/braid/server.key \
524+ --cap-dir /var/lib/braid/caps \
525+ --users mtelvers,avsm,samoht \
526 --opam-repo /home/user/opam-repository \
527 --cache-dir /var/cache/day10
528```
···531- `--port PORT` - Port to listen on (required)
532- `--public-addr HOST` - Public hostname for the capability URI (required)
533- `--key-file PATH` - Path to store/load the server's secret key (default: server.key)
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)
536- `--opam-repo PATH` - Path to opam-repository
537- `--cache-dir PATH` - Cache directory for day10
538539The `--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.
540541+**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`
544545+### 2. Distribute the Capability Files
546+547+Copy each user's capability file to their client machine:
548549```bash
550+scp build.example.com:/var/lib/braid/caps/mtelvers.cap ~/.config/braid.cap
551```
552553+Each capability file contains a URI like:
554```
555capnp://sha-256:abc123...@build.example.com:5000/def456...
556```
557558+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.
561562### 3. Run Remote Merge Tests
563···627braid server --port 5000 \
628 --public-addr basil.caelum.ci.dev \
629 --key-file ~/braid-server.key \
630+ --cap-dir ~/caps \
631+ --users mtelvers,avsm \
632 --opam-repo ~/opam-repository \
633 --cache-dir /var/cache/day10
634635+# Distribute capabilities (once per user)
636+scp basil.caelum.ci.dev:~/caps/mtelvers.cap ~/.config/braid.cap
637638# From any client - test an overlay
639braid merge-test https://github.com/mtelvers/claude-repo \
+19-6
bin/main.ml
···224 let doc = "Path to secret key file (created if doesn't exist)" in
225 Arg.(value & opt string "braid.key" & info ["key-file"] ~docv:"FILE" ~doc)
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)
0000230 in
231 let listen_addr_arg =
232 let doc = "Address to listen on" in
···241 Arg.(value & opt string "/var/cache/day10" & info ["cache-dir"] ~docv:"PATH" ~doc)
242 in
243244- let server _setup port public_addr key_file cap_file listen_addr opam_repo cache_dir =
000000000245 Eio_main.run @@ fun env ->
246 Eio.Switch.run @@ fun sw ->
247 let net = Eio.Stdenv.net env in
248 let fs = Eio.Stdenv.cwd env in
249 Server.run ~sw ~net ~fs ~listen_addr ~listen_port:port ~public_addr
250- ~key_file ~cap_file ~opam_repo_path:opam_repo ~cache_dir
251 in
252253 let doc = "Start RPC server for remote braid execution" in
254 let info = Cmd.info "server" ~doc in
255 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)
257258(* Query: failures *)
259let failures_cmd =
···224 let doc = "Path to secret key file (created if doesn't exist)" in
225 Arg.(value & opt string "braid.key" & info ["key-file"] ~docv:"FILE" ~doc)
226 in
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)
234 in
235 let listen_addr_arg =
236 let doc = "Address to listen on" in
···245 Arg.(value & opt string "/var/cache/day10" & info ["cache-dir"] ~docv:"PATH" ~doc)
246 in
247248+ 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;
258 Eio_main.run @@ fun env ->
259 Eio.Switch.run @@ fun sw ->
260 let net = Eio.Stdenv.net env in
261 let fs = Eio.Stdenv.cwd env in
262 Server.run ~sw ~net ~fs ~listen_addr ~listen_port:port ~public_addr
263+ ~key_file ~cap_dir ~users ~opam_repo_path:opam_repo ~cache_dir
264 in
265266 let doc = "Start RPC server for remote braid execution" in
267 let info = Cmd.info "server" ~doc in
268 Cmd.v info Term.(const server $ setup_log_term $ port_arg $ public_addr_arg
269+ $ key_file_arg $ cap_dir_arg $ users_arg $ listen_addr_arg $ opam_repo $ cache_dir)
270271(* Query: failures *)
272let failures_cmd =
+27-6
lib/server.ml
···1(** Cap'n Proto RPC server for BraidService *)
23-(** 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 =
5 let service = Rpc_service.local ~opam_repo_path ~cache_dir in
6 let addr = `TCP (listen_addr, listen_port) in
7 let public_address = `TCP (public_addr, listen_port) in
8 let secret_key = `File (Eio.Path.(fs / key_file)) in
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
0000000000012 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;
000000000014 Fmt.pr "Server listening on %s:%d@." listen_addr listen_port;
15 Fmt.pr " Public address: %s:%d@." public_addr listen_port;
16 Fmt.pr " Key file: %s@." key_file;
17- Fmt.pr " Capability file: %s@." cap_file;
18 (* Block forever - server runs until killed *)
19 Eio.Fiber.await_cancel ()
···1(** Cap'n Proto RPC server for BraidService *)
23+(** 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 let service = Rpc_service.local ~opam_repo_path ~cache_dir in
6 let addr = `TCP (listen_addr, listen_port) in
7 let public_address = `TCP (public_addr, listen_port) in
8 let secret_key = `File (Eio.Path.(fs / key_file)) in
9 let config = Capnp_rpc_unix.Vat_config.create ~secret_key ~public_address ~net addr 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
23 let vat = Capnp_rpc_unix.serve ~sw ~restore config in
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+35 Fmt.pr "Server listening on %s:%d@." listen_addr listen_port;
36 Fmt.pr " Public address: %s:%d@." public_addr listen_port;
37 Fmt.pr " Key file: %s@." key_file;
38+ Fmt.pr " Users: %s@." (String.concat ", " users);
39 (* Block forever - server runs until killed *)
40 Eio.Fiber.await_cancel ()