daa3bf69
refactor: the daemon owns its command surface, shell integration, upgrade and QUIC arm as child files
a73x 2026-08-30 12:01
Commit message
build.zig
| Old | New | ||
|---|---|---|---|
| @@ -159,10 +159,6 @@ const mod_table = [_]ModSpec{ | |||
| 159 | // side-channel events. The borrowed clipboard slice stays tied to the | 159 | // side-channel events. The borrowed clipboard slice stays tied to the |
| 160 | // frame payload, so native and wasm consumers share one contract. | 160 | // frame payload, so native and wasm consumers share one contract. |
| 161 | .{ .name = "client_core", .path = "src/client/client_core.zig", .layer = 1, .wasm = true, .imports = &.{"protocol"} }, | 161 | .{ .name = "client_core", .path = "src/client/client_core.zig", .layer = 1, .wasm = true, .imports = &.{"protocol"} }, |
| 162 | // The QUIC listener: the vocabulary plus a UDP socket and a connection | ||
| 163 | // table, and deliberately no protocol import — it carries opaque bytes, | ||
| 164 | // exactly as proxy.zig does. | ||
| 165 | .{ .name = "quic_server", .path = "src/server/quic_server.zig", .layer = 1, .link_libc = true, .imports = &.{"quic"}, .quic_tests = true }, | ||
| 166 | // This image, as a path something can exec. Under src/cli/ because it | 162 | // This image, as a path something can exec. Under src/cli/ because it |
| 167 | // asks the OS about the process it is in — a question no headless | 163 | // asks the OS about the process it is in — a question no headless |
| 168 | // client may spell. | 164 | // client may spell. |
| @@ -182,28 +178,12 @@ const mod_table = [_]ModSpec{ | |||
| 182 | // protocol and nothing else — no daemon, no clients — so the tracker's | 178 | // protocol and nothing else — no daemon, no clients — so the tracker's |
| 183 | // own tests drive it with an engine and no socket in sight. | 179 | // own tests drive it with an engine and no socket in sight. |
| 184 | .{ .name = "delta", .path = "src/engine/delta.zig", .layer = 1, .imports = &.{ "engine", "protocol" } }, | 180 | .{ .name = "delta", .path = "src/engine/delta.zig", .layer = 1, .imports = &.{ "engine", "protocol" } }, |
| 185 | // The session's command state machine: MarkEvents in, transitions out. | ||
| 186 | // Engine plus protocol and nothing else, same shape as delta — pure, | ||
| 187 | // socket-free, and its own tests drive it with no daemon in sight. | ||
| 188 | .{ .name = "cmd", .path = "src/server/cmd.zig", .layer = 1, .imports = &.{ "engine", "protocol" } }, | ||
| 189 | // The wall: the host spelling grammar, the daemon list, and the state | 181 | // The wall: the host spelling grammar, the daemon list, and the state |
| 190 | // file behind them, plus the one atomic writer every state file uses. | 182 | // file behind them, plus the one atomic writer every state file uses. |
| 191 | // One owner for a grammar the hub, the file and the CLI all have to | 183 | // One owner for a grammar the hub, the file and the CLI all have to |
| 192 | // agree on — xdg for where that file lives, so its tests need no hub, | 184 | // agree on — xdg for where that file lives, so its tests need no hub, |
| 193 | // no daemon and no socket. | 185 | // no daemon and no socket. |
| 194 | .{ .name = "hosts", .path = "src/client/hosts.zig", .layer = 1, .imports = &.{"xdg"}, .test_imports = &.{"testtmp"} }, | 186 | .{ .name = "hosts", .path = "src/client/hosts.zig", .layer = 1, .imports = &.{"xdg"}, .test_imports = &.{"testtmp"} }, |
| 195 | // The upgrade vocabulary: the version skew rule and the manifest an | ||
| 196 | // exec-ing daemon leaves for its replacement. Protocol only — the | ||
| 197 | // manifest is a stranger's format (length-prefixed sections, unknown | ||
| 198 | // tags skipped), so it cannot depend on server types. | ||
| 199 | .{ .name = "upgrade", .path = "src/server/upgrade.zig", .layer = 1, .imports = &.{"protocol"} }, | ||
| 200 | // Shell integration: the OSC 133 mark scripts and what a spawn must add | ||
| 201 | // to hand them to a shell. Near-leaf on purpose — it writes files and | ||
| 202 | // reads the environment, and knows nothing of ptys, servers or the | ||
| 203 | // protocol, so its tests need no daemon and no socket. The one import | ||
| 204 | // is xdg, for the private-directory policy the shim directory shares | ||
| 205 | // with the key file's parent; xdg is itself a leaf, so no cycle. | ||
| 206 | .{ .name = "shellint", .path = "src/server/shellint.zig", .layer = 1, .imports = &.{"xdg"} }, | ||
| 207 | // The replay core: snapshot/delta application and the resume | 187 | // The replay core: snapshot/delta application and the resume |
| 208 | // coordinates, shared by the CLI client, the wasm core, and the | 188 | // coordinates, shared by the CLI client, the wasm core, and the |
| 209 | // server's test fixtures. Engine plus protocol and nothing else, and | 189 | // server's test fixtures. Engine plus protocol and nothing else, and |
| @@ -223,11 +203,18 @@ const mod_table = [_]ModSpec{ | |||
| 223 | // under it. | 203 | // under it. |
| 224 | .{ .name = "ptyclient", .path = "test/ptyclient.zig", .layer = 1, .link_libc = true, .imports = &.{ "pty", "script" } }, | 204 | .{ .name = "ptyclient", .path = "test/ptyclient.zig", .layer = 1, .link_libc = true, .imports = &.{ "pty", "script" } }, |
| 225 | // ---- layer 2 ---- | 205 | // ---- layer 2 ---- |
| 226 | // quic and quic_server both: the listener it owns, and the vocabulary | 206 | // The command state machine, shell integration, the upgrade vocabulary |
| 227 | // it names directly (the key it loads, the idle default it falls back | 207 | // and the QUIC listener are CHILD FILES of this root rather than modules |
| 228 | // to). xdg is for endpoint_req's lazy bind — the default key path, | 208 | // of their own, so nothing outside src/server/ can name one: a second |
| 229 | // resolved by the daemon itself when nobody handed it a --key. | 209 | // module claiming any of those files is a file-in-multiple-modules |
| 230 | .{ .name = "server", .path = "src/server/server.zig", .layer = 2, .link_libc = true, .imports = &.{ "engine", "pty", "protocol", "delta", "cmd", "shellint", "sockpath", "quic", "quic_server", "xdg", "upgrade", "proxy" }, .test_imports = &.{ "replica", "testtmp" }, .quic_tests = true }, | 210 | // compile error. `quic` is the listener's and the daemon's both — the |
| 211 | // socket that child opens, and the vocabulary this root names directly | ||
| 212 | // (the key it loads, the idle default it falls back to). xdg is for | ||
| 213 | // endpoint_req's lazy bind — the default key path, resolved by the | ||
| 214 | // daemon itself when nobody handed it a --key — and for the shim | ||
| 215 | // directory shell integration writes under the same 0700 policy. | ||
| 216 | // `pty` stays a row of its own: the ptyclient fixture consumes it. | ||
| 217 | .{ .name = "daemon", .path = "src/server/server.zig", .layer = 2, .link_libc = true, .imports = &.{ "engine", "pty", "protocol", "delta", "sockpath", "quic", "xdg", "proxy" }, .test_imports = &.{ "replica", "testtmp" }, .quic_tests = true }, | ||
| 231 | // The agent-facing client. It speaks frames and owns no terminal, which | 218 | // The agent-facing client. It speaks frames and owns no terminal, which |
| 232 | // is the whole point — it attaches at 0x0 and never claims the grid. | 219 | // is the whole point — it attaches at 0x0 and never claims the grid. |
| 233 | // The transport modules are the CLI client's, minus everything that | 220 | // The transport modules are the CLI client's, minus everything that |
| @@ -247,13 +234,14 @@ const mod_table = [_]ModSpec{ | |||
| 247 | // wall and the browser hub resolve a host line the same way. Nothing | 234 | // wall and the browser hub resolve a host line the same way. Nothing |
| 248 | // here WRITES that file — `wall_host.recordHost` and `hub_main` do. | 235 | // here WRITES that file — `wall_host.recordHost` and `hub_main` do. |
| 249 | .{ .name = "client", .path = "src/client/client.zig", .layer = 3, .link_libc = true, .imports = &.{ "protocol", "replica", "keymap", "quic", "handoff", "hosts", "xdg", "sockpath", "askpass" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, | 236 | .{ .name = "client", .path = "src/client/client.zig", .layer = 3, .link_libc = true, .imports = &.{ "protocol", "replica", "keymap", "quic", "handoff", "hosts", "xdg", "sockpath", "askpass" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, |
| 250 | // The daemon entrypoint loads the key and constructs the listener, so | 237 | // The daemon entrypoint loads the key and constructs the listener, so it |
| 251 | // it needs quic/quic_server directly rather than through the server. | 238 | // needs `quic` directly; the listener type and the upgrade vocabulary it |
| 252 | // `muxd endpoint` prints the announce line handoff spells; sockpath is | 239 | // reaches through the daemon root's re-exports, being that root's own |
| 253 | // the sun_path bound, checked before any verb acts on the path; and the | 240 | // child files now. `muxd endpoint` prints the announce line handoff |
| 254 | // keygen round-trip test needs a directory to generate into, which the | 241 | // spells; sockpath is the sun_path bound, checked before any verb acts |
| 255 | // daemon itself never touches. | 242 | // on the path; and the keygen round-trip test needs a directory to |
| 256 | .{ .name = "daemon_main", .path = "src/cli/main.zig", .layer = 3, .link_libc = true, .imports = &.{ "server", "protocol", "proxy", "quic", "quic_server", "xdg", "spawn", "handoff", "sockpath", "upgrade", "cliflags" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, | 243 | // generate into, which the daemon itself never touches. |
| 244 | .{ .name = "daemon_main", .path = "src/cli/main.zig", .layer = 3, .link_libc = true, .imports = &.{ "daemon", "protocol", "proxy", "quic", "xdg", "spawn", "handoff", "sockpath", "cliflags" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, | ||
| 257 | // ---- layer 4 ---- | 245 | // ---- layer 4 ---- |
| 258 | // The muxweb hub's HTTP/WebSocket decisions: Origin gate, route | 246 | // The muxweb hub's HTTP/WebSocket decisions: Origin gate, route |
| 259 | // table, WS endpoint naming. Assets are injected (the exe root | 247 | // table, WS endpoint naming. Assets are injected (the exe root |
| @@ -820,24 +808,24 @@ fn docGate(b: *std.Build, target: std.Build.ResolvedTarget, check_step: *std.Bui | |||
| 820 | } | 808 | } |
| 821 | 809 | ||
| 822 | /// Test registration order. Doctrine-laden and deliberately NOT derived | 810 | /// Test registration order. Doctrine-laden and deliberately NOT derived |
| 823 | /// from the layers: delta, cmd, shellint and sockpath run BEFORE server | 811 | /// from the layers: delta and sockpath run BEFORE daemon because their |
| 824 | /// because their tests are seconds-long and socket-free, while a | 812 | /// tests are seconds-long and socket-free, while a regression in either |
| 825 | /// regression in any of them can wedge a server test that waits on a | 813 | /// can wedge a daemon test that waits on a client forever — and a wedged |
| 826 | /// client forever — and a wedged step prints nothing at all. Failing | 814 | /// step prints nothing at all. Failing first is what makes the catch |
| 827 | /// first is what makes the catch legible. script leads for the same | 815 | /// legible; the daemon's own child files obey the same rule inside its |
| 828 | /// reason: instant, allocation-only, and both fixtures inherit its | 816 | /// binary, where their imports lead the root's test block. script leads |
| 829 | /// escape pins. The mains are roots of nothing but the one binary, and | 817 | /// for the same reason: instant, allocation-only, and both fixtures |
| 830 | /// they carry the argument parsers — a test that is never built is not a | 818 | /// inherit its escape pins. The mains are roots of nothing but the one |
| 831 | /// test (decisions.md). | 819 | /// binary, and they carry the argument parsers — a test that is never |
| 820 | /// built is not a test (decisions.md). | ||
| 832 | const test_order = [_][]const u8{ | 821 | const test_order = [_][]const u8{ |
| 833 | "script", "protocol", "client_core", "engine", "pty", | 822 | "script", "protocol", "client_core", "engine", "pty", |
| 834 | "delta", "cmd", "hosts", "askpass", "upgrade", | 823 | "delta", "hosts", "askpass", "replica", "keymap", |
| 835 | "shellint", "replica", "keymap", "webhub", "wall", | 824 | "webhub", "wall", "sockpath", "agent_main", "daemon", |
| 836 | "sockpath", "agent_main", "server", "client", "proxy", | 825 | "client", "proxy", "client_main", "quic", "daemon_main", |
| 837 | "client_main", "quic", "quic_server", "daemon_main", "testtmp", | 826 | "testtmp", "rawmode", "delaypipe", "xdg", "spawn", |
| 838 | "rawmode", "delaypipe", "xdg", "spawn", "handoff", | 827 | "handoff", "layout", "render", "ptyclient", "hub_main", |
| 839 | "layout", "render", "ptyclient", "hub_main", "wsclient", | 828 | "wsclient", "cliflags", "mux", |
| 840 | "cliflags", "mux", | ||
| 841 | }; | 829 | }; |
| 842 | 830 | ||
| 843 | comptime { | 831 | comptime { |
src/cli/main.zig
| Old | New | ||
|---|---|---|---|
| @@ -5,17 +5,17 @@ | |||
| 5 | // shell, and `/bin/sh` is the name it falls back to when $SHELL says nothing. | 5 | // shell, and `/bin/sh` is the name it falls back to when $SHELL says nothing. |
| 6 | 6 | ||
| 7 | const std = @import("std"); | 7 | const std = @import("std"); |
| 8 | const Server = @import("server").Server; | 8 | const Server = @import("daemon").Server; |
| 9 | const proto = @import("protocol"); | 9 | const proto = @import("protocol"); |
| 10 | const proxy = @import("proxy"); | 10 | const proxy = @import("proxy"); |
| 11 | const quic = @import("quic"); | 11 | const quic = @import("quic"); |
| 12 | const quic_server = @import("quic_server"); | 12 | const quic_server = @import("daemon").quic_server; |
| 13 | const build_options = @import("build_options"); | 13 | const build_options = @import("build_options"); |
| 14 | const xdg = @import("xdg"); | 14 | const xdg = @import("xdg"); |
| 15 | const spawn = @import("spawn"); | 15 | const spawn = @import("spawn"); |
| 16 | const handoff = @import("handoff"); | 16 | const handoff = @import("handoff"); |
| 17 | const sockpath = @import("sockpath"); | 17 | const sockpath = @import("sockpath"); |
| 18 | const upgrade = @import("upgrade"); | 18 | const upgrade = @import("daemon").upgrade; |
| 19 | const cliflags = @import("cliflags"); | 19 | const cliflags = @import("cliflags"); |
| 20 | 20 | ||
| 21 | const usage = | 21 | const usage = |
| @@ -527,7 +527,7 @@ fn resumeRun(alloc: std.mem.Allocator, o: Opts, resume_fd: std.posix.fd_t) !u8 { | |||
| 527 | // The marker's job ended the moment this image started serving. | 527 | // The marker's job ended the moment this image started serving. |
| 528 | _ = unsetenv(rollback_marker); | 528 | _ = unsetenv(rollback_marker); |
| 529 | 529 | ||
| 530 | @import("server").installSignalHandlers(); | 530 | @import("daemon").installSignalHandlers(); |
| 531 | return try srv.run(); | 531 | return try srv.run(); |
| 532 | } | 532 | } |
| 533 | 533 | ||
| @@ -678,7 +678,7 @@ fn run(alloc: std.mem.Allocator, o: Opts, sock_path: []const u8) !u8 { | |||
| 678 | srv.attachQuic(l); | 678 | srv.attachQuic(l); |
| 679 | } | 679 | } |
| 680 | 680 | ||
| 681 | @import("server").installSignalHandlers(); | 681 | @import("daemon").installSignalHandlers(); |
| 682 | return try srv.run(); | 682 | return try srv.run(); |
| 683 | } | 683 | } |
| 684 | 684 | ||
src/server/server.zig
| Old | New | ||
|---|---|---|---|
| @@ -13,16 +13,18 @@ const Engine = @import("engine").Engine; | |||
| 13 | const Pty = @import("pty").Pty; | 13 | const Pty = @import("pty").Pty; |
| 14 | const proto = @import("protocol"); | 14 | const proto = @import("protocol"); |
| 15 | const DeltaTracker = @import("delta").DeltaTracker; | 15 | const DeltaTracker = @import("delta").DeltaTracker; |
| 16 | const cmdmod = @import("cmd"); | 16 | const cmdmod = @import("cmd.zig"); |
| 17 | const shellint = @import("shellint"); | 17 | const shellint = @import("shellint.zig"); |
| 18 | // Test-only consumer (applyFrame): the tests replay daemon frames through | 18 | // Test-only consumer (applyFrame): the tests replay daemon frames through |
| 19 | // the production client's replay core rather than a hand-rolled twin. | 19 | // the production client's replay core rather than a hand-rolled twin. |
| 20 | const replica_mod = @import("replica"); | 20 | const replica_mod = @import("replica"); |
| 21 | const sockpath = @import("sockpath"); | 21 | const sockpath = @import("sockpath"); |
| 22 | const quic = @import("quic"); | 22 | const quic = @import("quic"); |
| 23 | const quic_server = @import("quic_server"); | ||
| 24 | const xdg = @import("xdg"); | 23 | const xdg = @import("xdg"); |
| 25 | const upgrade = @import("upgrade"); | 24 | // Re-exported for the daemon's own main (src/cli/main.zig) — the only |
| 25 | // consumer outside this folder; nobody else may know these exist. | ||
| 26 | pub const quic_server = @import("quic_server.zig"); | ||
| 27 | pub const upgrade = @import("upgrade.zig"); | ||
| 26 | const proxy = @import("proxy"); | 28 | const proxy = @import("proxy"); |
| 27 | const TmpDir = @import("testtmp").TmpDir; | 29 | const TmpDir = @import("testtmp").TmpDir; |
| 28 | // The agent relay is a sub-file of this module, not a row of its own: it is | 30 | // The agent relay is a sub-file of this module, not a row of its own: it is |
| @@ -3600,6 +3602,15 @@ pub const Server = struct { | |||
| 3600 | test { | 3602 | test { |
| 3601 | std.testing.refAllDeclsRecursive(@This()); | 3603 | std.testing.refAllDeclsRecursive(@This()); |
| 3602 | 3604 | ||
| 3605 | // The child files' own suites first: seconds-long and socket-free, while | ||
| 3606 | // a regression in one can wedge a daemon test that waits on a client | ||
| 3607 | // forever — and a wedged step prints nothing at all, so failing before | ||
| 3608 | // the sockets open is what makes the catch legible. | ||
| 3609 | _ = @import("cmd.zig"); | ||
| 3610 | _ = @import("shellint.zig"); | ||
| 3611 | _ = @import("upgrade.zig"); | ||
| 3612 | _ = @import("quic_server.zig"); | ||
| 3613 | |||
| 3603 | // Reaching a file is what registers its tests; build.zig gates the list. | 3614 | // Reaching a file is what registers its tests; build.zig gates the list. |
| 3604 | _ = @import("server_test_agent.zig"); | 3615 | _ = @import("server_test_agent.zig"); |
| 3605 | _ = @import("server_test_attach.zig"); | 3616 | _ = @import("server_test_attach.zig"); |
src/server/server_test_agent.zig
| Old | New | ||
|---|---|---|---|
| @@ -1,7 +1,7 @@ | |||
| 1 | const std = @import("std"); | 1 | const std = @import("std"); |
| 2 | const proto = @import("protocol"); | 2 | const proto = @import("protocol"); |
| 3 | const quic = @import("quic"); | 3 | const quic = @import("quic"); |
| 4 | const quic_server = @import("quic_server"); | 4 | const quic_server = @import("quic_server.zig"); |
| 5 | const TmpDir = @import("testtmp").TmpDir; | 5 | const TmpDir = @import("testtmp").TmpDir; |
| 6 | const h = @import("server_test_harness.zig"); | 6 | const h = @import("server_test_harness.zig"); |
| 7 | const srv_mod = @import("server.zig"); | 7 | const srv_mod = @import("server.zig"); |
src/server/server_test_await.zig
| Old | New | ||
|---|---|---|---|
| @@ -1,6 +1,6 @@ | |||
| 1 | const std = @import("std"); | 1 | const std = @import("std"); |
| 2 | const proto = @import("protocol"); | 2 | const proto = @import("protocol"); |
| 3 | const shellint = @import("shellint"); | 3 | const shellint = @import("shellint.zig"); |
| 4 | const TmpDir = @import("testtmp").TmpDir; | 4 | const TmpDir = @import("testtmp").TmpDir; |
| 5 | const h = @import("server_test_harness.zig"); | 5 | const h = @import("server_test_harness.zig"); |
| 6 | const srv_mod = @import("server.zig"); | 6 | const srv_mod = @import("server.zig"); |
src/server/server_test_harness.zig
| Old | New | ||
|---|---|---|---|
| @@ -3,7 +3,7 @@ const Engine = @import("engine").Engine; | |||
| 3 | const proto = @import("protocol"); | 3 | const proto = @import("protocol"); |
| 4 | const replica_mod = @import("replica"); | 4 | const replica_mod = @import("replica"); |
| 5 | const quic = @import("quic"); | 5 | const quic = @import("quic"); |
| 6 | const quic_server = @import("quic_server"); | 6 | const quic_server = @import("quic_server.zig"); |
| 7 | const TmpDir = @import("testtmp").TmpDir; | 7 | const TmpDir = @import("testtmp").TmpDir; |
| 8 | const srv_mod = @import("server.zig"); | 8 | const srv_mod = @import("server.zig"); |
| 9 | const Server = srv_mod.Server; | 9 | const Server = srv_mod.Server; |
src/server/server_test_quic.zig
| Old | New | ||
|---|---|---|---|
| @@ -2,7 +2,7 @@ const std = @import("std"); | |||
| 2 | const Engine = @import("engine").Engine; | 2 | const Engine = @import("engine").Engine; |
| 3 | const proto = @import("protocol"); | 3 | const proto = @import("protocol"); |
| 4 | const quic = @import("quic"); | 4 | const quic = @import("quic"); |
| 5 | const quic_server = @import("quic_server"); | 5 | const quic_server = @import("quic_server.zig"); |
| 6 | const TmpDir = @import("testtmp").TmpDir; | 6 | const TmpDir = @import("testtmp").TmpDir; |
| 7 | const h = @import("server_test_harness.zig"); | 7 | const h = @import("server_test_harness.zig"); |
| 8 | const srv_mod = @import("server.zig"); | 8 | const srv_mod = @import("server.zig"); |
src/server/server_test_session.zig
| Old | New | ||
|---|---|---|---|
| @@ -2,7 +2,7 @@ const std = @import("std"); | |||
| 2 | const Engine = @import("engine").Engine; | 2 | const Engine = @import("engine").Engine; |
| 3 | const proto = @import("protocol"); | 3 | const proto = @import("protocol"); |
| 4 | const quic = @import("quic"); | 4 | const quic = @import("quic"); |
| 5 | const quic_server = @import("quic_server"); | 5 | const quic_server = @import("quic_server.zig"); |
| 6 | const xdg = @import("xdg"); | 6 | const xdg = @import("xdg"); |
| 7 | const TmpDir = @import("testtmp").TmpDir; | 7 | const TmpDir = @import("testtmp").TmpDir; |
| 8 | const h = @import("server_test_harness.zig"); | 8 | const h = @import("server_test_harness.zig"); |
src/server/server_test_upgrade.zig
| Old | New | ||
|---|---|---|---|
| @@ -1,6 +1,6 @@ | |||
| 1 | const std = @import("std"); | 1 | const std = @import("std"); |
| 2 | const proto = @import("protocol"); | 2 | const proto = @import("protocol"); |
| 3 | const upgrade = @import("upgrade"); | 3 | const upgrade = @import("upgrade.zig"); |
| 4 | const TmpDir = @import("testtmp").TmpDir; | 4 | const TmpDir = @import("testtmp").TmpDir; |
| 5 | const h = @import("server_test_harness.zig"); | 5 | const h = @import("server_test_harness.zig"); |
| 6 | const srv_mod = @import("server.zig"); | 6 | const srv_mod = @import("server.zig"); |