1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
|
//! 9harness: the harness fs daemon — a long-running 9P2000 server that
//! mirrors every AI-agent harness's state (Claude Code, Codex, omp,
//! hermes, dsh + their skills) as one read-only, fresh-from-disk tree.
//! See docs/DESIGN.md for the tree contract and src/tree.zig for the tree.
//!
//! By default it posts itself under the name `harness` with
//! `serve.Runner.listenPosted`, so it appears at $XDG_RUNTIME_DIR/9p/harness
//! and every interactive fish (self-wrapped in `9ns --mntgen`) sees it at
//! /mnt/9p/harness with zero configuration. `--unix`, `--tcp` and `--fd`
//! are the other listen forms; `--no-post` serves without posting; the
//! five harness roots default to $HOME/.<name> and `--root NAME=PATH`
//! pins any of them elsewhere (the tests use fake homes).
//!
//! v1 has no daemonization: run it in a zmx session, the zmx way:
//!
//! zmx run harness -d 9harness
//!
//! SIGTERM or SIGINT stops it cleanly (a posted name is unposted).
const std = @import("std");
const builtin = @import("builtin");
const cloud9 = @import("cloud9");
const tree = @import("tree.zig");
const fs = cloud9.fs;
const serve = cloud9.serve;
const post = cloud9.post;
const transport = cloud9.transport;
const Io = std.Io;
const linux = std.os.linux;
const usage_text =
\\usage: 9harness [--unix PATH | --tcp IP:PORT | --fd N] [--no-post]
\\ [--name NAME] [--root NAME=PATH]...
\\
\\A read-only, fresh-from-disk 9P2000 view of every harness's state:
\\ /pid /uptime daemon facts
\\ /claude/{projects,history,skills}
\\ /codex/{sessions,session-index,history}
\\ /omp /hermes /dsh full mirrors, raw
\\ /skills/{claude,codex,omp} the union skills view
\\
\\By default the daemon posts itself under the name `harness`, so it is
\\dialable at $XDG_RUNTIME_DIR/9p/harness and mountable by 9ns --mntgen
\\at /mnt/9p/harness. --no-post skips posting; --unix/--tcp add plain
\\listeners beside the post; --fd N serves one 9P session over the
\\connected stream on descriptor N and posts nothing.
\\--root NAME=PATH pins one harness root (NAME: claude, codex, omp,
\\hermes, dsh) somewhere other than $HOME/.<NAME>; repeatable.
\\
\\Run it in a zmx session, the zmx way: `zmx run harness -d 9harness`.
\\
;
const max_connections = 16;
const Runner = serve.Runner(tree.Harness, tree.opts, .{
.msize = tree.msize,
.connections = max_connections,
.listeners = 2, // the posted name plus one of --unix/--tcp
});
// Static memory, the 9proc-demo way: the harness and the runner are large
// (the path table, the per-connection buffers) and live in .bss.
var harness_mem: tree.Harness = undefined;
var runner_mem: Runner = undefined;
var stop_requested = std.atomic.Value(bool).init(false);
fn onSignal(sig: linux.SIG) callconv(.c) void {
_ = sig;
stop_requested.store(true, .release);
}
fn installSignalHandlers() void {
const act = linux.Sigaction{
.handler = .{ .handler = onSignal },
.mask = @splat(0),
.flags = 0,
};
_ = linux.sigaction(.TERM, &act, null);
_ = linux.sigaction(.INT, &act, null);
const ign = linux.Sigaction{
.handler = .{ .handler = linux.SIG.IGN },
.mask = @splat(0),
.flags = 0,
};
_ = linux.sigaction(.PIPE, &ign, null);
}
// ---- the serve handler ---------------------------------------------------------
fn serveReq(ctx: ?*anyopaque, conn: *Runner.Conn, req: fs.Req) void {
const h: *tree.Harness = @ptrCast(@alignCast(ctx.?));
// The answer's bytes point into the harness's shared buffers, so the
// mutex spans handle and reply: the engine copies them into the
// connection's output before the lock goes.
h.mutex.lockUncancelable(h.io);
const a = tree.handle(h, req);
conn.reply(&a.reply, a.bytes);
h.mutex.unlock(h.io);
}
// ---- the CLI --------------------------------------------------------------------
const Mode = enum { posted, unix, tcp, fd };
pub fn main(init: std.process.Init) !void {
run(init) catch |e| switch (e) {
// Both are reported with their own line above; a returned error
// would bury it under a Debug-build stack trace.
error.Usage, error.AlreadyPosted => std.process.exit(1),
else => return e,
};
}
fn run(init: std.process.Init) !void {
const io = init.io;
const arena = init.arena.allocator();
const args = try init.minimal.args.toSlice(arena);
const envp: post.Env = init.minimal.environ.block.slice.ptr;
var mode: Mode = .posted;
var unix_path: []const u8 = "";
var tcp_addr: []const u8 = "";
var fd_no: i32 = -1;
var post_name: []const u8 = "harness";
var no_post = false;
var base_overrides: [5]?[]const u8 = @splat(null);
var i: usize = 1;
while (i < args.len) : (i += 1) {
const a = args[i];
if (std.mem.eql(u8, a, "--no-post")) {
no_post = true;
} else if (std.mem.eql(u8, a, "--unix") or std.mem.eql(u8, a, "--tcp") or
std.mem.eql(u8, a, "--fd") or std.mem.eql(u8, a, "--name") or
std.mem.eql(u8, a, "--root"))
{
i += 1;
if (i >= args.len) {
std.debug.print("9harness: {s} needs an argument\n{s}", .{ a, usage_text });
return error.Usage;
}
const v = args[i];
if (std.mem.eql(u8, a, "--unix")) {
mode = .unix;
unix_path = v;
} else if (std.mem.eql(u8, a, "--tcp")) {
mode = .tcp;
tcp_addr = v;
} else if (std.mem.eql(u8, a, "--fd")) {
mode = .fd;
fd_no = std.fmt.parseInt(i32, v, 10) catch {
std.debug.print("9harness: --fd: not a number: {s}\n", .{v});
return error.Usage;
};
} else if (std.mem.eql(u8, a, "--name")) {
post_name = v;
if (!post.legalName(post_name)) {
std.debug.print("9harness: illegal post name: {s}\n", .{v});
return error.Usage;
}
} else {
const eq = std.mem.indexOfScalar(u8, v, '=') orelse {
std.debug.print("9harness: --root NAME=PATH: {s}\n{s}", .{ v, usage_text });
return error.Usage;
};
const name = v[0..eq];
const root = std.meta.stringToEnum(tree.Root, name) orelse {
std.debug.print("9harness: unknown root {s} (claude, codex, omp, hermes, dsh)\n", .{name});
return error.Usage;
};
base_overrides[@intFromEnum(root)] = v[eq + 1 ..];
}
} else if (std.mem.eql(u8, a, "--help") or std.mem.eql(u8, a, "-h")) {
std.debug.print("{s}", .{usage_text});
return;
} else {
std.debug.print("9harness: unknown argument {s}\n{s}", .{ a, usage_text });
return error.Usage;
}
}
// The five pinned roots: $HOME/.<name> unless overridden.
const home = post.getenv(envp, "HOME");
var bases: [5][]const u8 = @splat("");
inline for (0..5) |r| {
if (base_overrides[r]) |path| {
bases[r] = path;
} else if (home) |hm| {
bases[r] = std.fmt.bufPrint(&root_bufs[r], "{s}/.{s}", .{ hm, root_dirs[r] }) catch return error.NameTooLong;
}
}
var any = false;
for (bases) |b| any = any or b.len != 0;
if (!any) {
std.debug.print("9harness: no harness roots (set $HOME or pass --root NAME=PATH)\n", .{});
return error.Usage;
}
harness_mem.init(.{ .io = io, .pid = @intCast(linux.getpid()), .bases = bases });
if (no_post and mode == .posted) {
std.debug.print("9harness: --no-post needs a listen form (--unix, --tcp or --fd)\n{s}", .{usage_text});
return error.Usage;
}
installSignalHandlers();
switch (mode) {
.fd => {
std.debug.print("9harness: serving one session on fd {d}\n", .{fd_no});
try serveFd(io, fd_no);
return;
},
.posted, .unix, .tcp => {},
}
runner_mem.init(.{ .io = io, .root = tree.root, .handler = .{ .ctx = &harness_mem, .serve = serveReq } });
defer runner_mem.stop();
var posted_something = false;
if (!no_post) {
runner_mem.listenPosted(envp, post_name, 16) catch |err| {
if (err == error.AlreadyPosted) {
std.debug.print("9harness: the name `{s}` is already posted by a live server\n", .{post_name});
return err;
}
std.debug.print("9harness: cannot post as {s}: {t}\n", .{ post_name, err });
return err;
};
posted_something = true;
var pbuf: [transport.sun_path_len]u8 = undefined;
const path = post.registryPath(envp, post_name, &pbuf) catch "";
std.debug.print("9harness: posted as {s} at {s}\n", .{ post_name, path });
}
switch (mode) {
.unix => {
_ = try runner_mem.listen(.{ .unix = try arena.dupeZ(u8, unix_path) }, 16);
std.debug.print("9harness: listening on {s}\n", .{unix_path});
},
.tcp => {
const addr = Io.net.IpAddress.parseLiteral(tcp_addr) catch {
std.debug.print("9harness: bad --tcp address: {s}\n", .{tcp_addr});
return error.Usage;
};
const bound = try runner_mem.listen(.{ .tcp = addr }, 16);
std.debug.print("9harness: listening on tcp!{f}\n", .{bound});
},
else => {},
}
var pinned: u32 = 0;
for (bases) |b| {
if (b.len != 0) pinned += 1;
}
std.debug.print("9harness: serving ({d} roots pinned, {d} bytes of tables)\n", .{ pinned, @sizeOf(tree.Harness) });
// The runner's tasks drive themselves; the main thread only waits for
// a stop signal (SIGTERM/SIGINT) and then unposts via stop().
while (!stop_requested.load(.acquire)) {
io.sleep(.fromMilliseconds(250), .awake) catch break;
}
std.debug.print("9harness: stopping\n", .{});
}
const root_dirs = [5][]const u8{ "claude", "codex", "omp", "hermes", "dsh" };
var root_bufs: [5][tree.base_capacity]u8 = @splat(@splat(0));
// ---- --fd N: one 9P session over a connected stream -----------------------------
/// Serves exactly one client on the connected stream of descriptor `n`
/// (socket activation, `9ns --spawn`-style handoffs), driving the engine
/// directly, the freestanding way. Returns when the client hangs up.
fn serveFd(io: Io, n: i32) !void {
const stream: Io.net.Stream = .{ .socket = .{ .handle = n, .address = .{ .ip4 = .loopback(0) } } };
const Engine = fs.Server(tree.Harness, tree.opts);
var in: [tree.msize]u8 = undefined;
var out: [2 * tree.msize]u8 = undefined;
var rbuf: [tree.msize]u8 = undefined;
var wbuf: [2 * tree.msize]u8 = undefined;
var stage: [tree.msize]u8 = undefined;
var engine = Engine.init(.{ .in = &in, .out = &out, .root = tree.root });
var reader = stream.reader(io, &rbuf);
var writer = stream.writer(io, &wbuf);
while (true) {
const frame = transport.readFrame(&reader.interface, &stage, tree.msize) catch return;
var off: usize = 0;
while (off < frame.len) {
off += engine.push(frame[off..]);
while (true) {
const req = engine.next() orelse break;
const a = answer(req);
engine.reply(&a.reply, a.bytes);
}
const pending = engine.output();
writer.interface.writeAll(pending) catch return;
engine.wrote(pending.len);
if (engine.protocol.dead) return;
}
writer.interface.flush() catch return;
}
}
/// One request for the --fd loop: same locking discipline as the runner's
/// handler (the reply bytes live in the harness's shared buffers).
fn answer(req: fs.Req) tree.Answer {
harness_mem.mutex.lockUncancelable(harness_mem.io);
defer harness_mem.mutex.unlock(harness_mem.io);
return tree.handle(&harness_mem, req);
}
test {
_ = @import("tree.zig"); // the tree's unit tests (exclusions, ids, ...)
}
test "main: the root override parser pins each named root" {
// Parsing is inline in run(); the mapping itself is what the tests
// rely on, and it is exercised end to end in test/e2e.sh.
_ = std.meta.stringToEnum(tree.Root, "claude").?;
_ = std.meta.stringToEnum(tree.Root, "codex").?;
_ = std.meta.stringToEnum(tree.Root, "omp").?;
_ = std.meta.stringToEnum(tree.Root, "hermes").?;
_ = std.meta.stringToEnum(tree.Root, "dsh").?;
try std.testing.expect(std.meta.stringToEnum(tree.Root, "zmx") == null);
}
|