Fence local profile capabilities
This commit is contained in:
@@ -8,6 +8,7 @@ const lsp = @import("lsp.zig");
|
||||
const mobile_acceptance = @import("mobile_acceptance.zig");
|
||||
const panel = @import("panel.zig");
|
||||
const pi_bridge = @import("pi_bridge.zig");
|
||||
const profile = @import("profile.zig");
|
||||
const protocol = @import("protocol.zig");
|
||||
const replay = @import("replay.zig");
|
||||
const repo = @import("repo.zig");
|
||||
@@ -26,6 +27,7 @@ const help_text =
|
||||
\\ mim [--help]
|
||||
\\ mim [--version]
|
||||
\\ mim context [task] [intent]
|
||||
\\ mim profile
|
||||
\\ mim mimctl context <socket|-> [task] [intent]
|
||||
\\ mim --mimctl <socket|-> <request...>
|
||||
\\
|
||||
@@ -71,6 +73,14 @@ pub fn main(init: std.process.Init) !u8 {
|
||||
try printLocalContext(allocator, init.io, &args, stdout);
|
||||
return 0;
|
||||
}
|
||||
if (std.mem.eql(u8, arg, "profile")) {
|
||||
try printProfile(allocator, init.io, stdout);
|
||||
return 0;
|
||||
}
|
||||
if (isProfileStub(arg)) {
|
||||
try printProfileStub(init.io, stderr, arg);
|
||||
return 69;
|
||||
}
|
||||
if (std.mem.eql(u8, arg, "mimctl")) {
|
||||
const sub = args.next() orelse {
|
||||
try stderr.writeStreamingAll(init.io, "mim: mimctl requires a subcommand\n");
|
||||
@@ -134,6 +144,26 @@ fn printLocalContext(allocator: std.mem.Allocator, io: std.Io, args: *std.proces
|
||||
try stdout.writeStreamingAll(io, text);
|
||||
}
|
||||
|
||||
fn printProfile(allocator: std.mem.Allocator, io: std.Io, stdout: std.Io.File) !void {
|
||||
const text = try profile.statusAlloc(allocator);
|
||||
defer allocator.free(text);
|
||||
try stdout.writeStreamingAll(io, text);
|
||||
}
|
||||
|
||||
fn isProfileStub(arg: []const u8) bool {
|
||||
return std.mem.eql(u8, arg, "remote") or
|
||||
std.mem.eql(u8, arg, "plugin") or
|
||||
std.mem.eql(u8, arg, "plugins") or
|
||||
std.mem.eql(u8, arg, "background-agent") or
|
||||
std.mem.eql(u8, arg, "bundled-pi");
|
||||
}
|
||||
|
||||
fn printProfileStub(io: std.Io, stderr: std.Io.File, arg: []const u8) !void {
|
||||
try stderr.writeStreamingAll(io, "mim: ");
|
||||
try stderr.writeStreamingAll(io, arg);
|
||||
try stderr.writeStreamingAll(io, " not built in this profile\n");
|
||||
}
|
||||
|
||||
fn requestMimctl(
|
||||
allocator: std.mem.Allocator,
|
||||
io: std.Io,
|
||||
@@ -193,6 +223,7 @@ test {
|
||||
_ = mobile_acceptance;
|
||||
_ = panel;
|
||||
_ = pi_bridge;
|
||||
_ = profile;
|
||||
_ = protocol;
|
||||
_ = replay;
|
||||
_ = repo;
|
||||
@@ -207,6 +238,14 @@ test "regular: help text names the binary and smoke boundary" {
|
||||
try std.testing.expect(std.mem.indexOf(u8, help_text, "mim - mobile-first terminal code editor") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, help_text, "canonical smoke path") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, help_text, "mim context") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, help_text, "mim profile") != null);
|
||||
}
|
||||
|
||||
test "regular: excluded profile capabilities have explicit stubs" {
|
||||
try std.testing.expect(isProfileStub("remote"));
|
||||
try std.testing.expect(isProfileStub("plugin"));
|
||||
try std.testing.expect(isProfileStub("background-agent"));
|
||||
try std.testing.expect(!isProfileStub("context"));
|
||||
}
|
||||
|
||||
test "regular: version output is stable enough for smoke checks" {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
const std = @import("std");
|
||||
|
||||
// Explicit v1 build/profile fences.
|
||||
// req: governance/002, governance/003, testing/001, testing/002
|
||||
|
||||
test {
|
||||
_ = statusAlloc;
|
||||
}
|
||||
|
||||
pub const active_profile = "dev-local";
|
||||
pub const local_socket = true;
|
||||
pub const remote_transport_built = false;
|
||||
pub const plugin_host_built = false;
|
||||
pub const bundled_pi_built = false;
|
||||
pub const background_agent_built = false;
|
||||
|
||||
pub fn statusAlloc(allocator: std.mem.Allocator) ![]u8 {
|
||||
var out = std.ArrayList(u8).empty;
|
||||
errdefer out.deinit(allocator);
|
||||
try appendLine(allocator, &out, "profile:v1");
|
||||
try appendKV(allocator, &out, "active_profile", active_profile);
|
||||
try appendKV(allocator, &out, "socket", if (local_socket) "local_only" else "disabled");
|
||||
try appendKV(allocator, &out, "remote_transport", if (remote_transport_built) "built" else "not_built");
|
||||
try appendKV(allocator, &out, "plugin_host", if (plugin_host_built) "built" else "not_built");
|
||||
try appendKV(allocator, &out, "bundled_pi", if (bundled_pi_built) "built" else "not_built");
|
||||
try appendKV(allocator, &out, "background_agent", if (background_agent_built) "built" else "not_built");
|
||||
try appendKV(allocator, &out, "tool_mutation", "protocol_logged");
|
||||
return out.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
fn appendKV(allocator: std.mem.Allocator, out: *std.ArrayList(u8), key: []const u8, value: []const u8) !void {
|
||||
try out.appendSlice(allocator, key);
|
||||
try out.append(allocator, '=');
|
||||
try out.appendSlice(allocator, value);
|
||||
try out.append(allocator, '\n');
|
||||
}
|
||||
|
||||
fn appendLine(allocator: std.mem.Allocator, out: *std.ArrayList(u8), line: []const u8) !void {
|
||||
try out.appendSlice(allocator, line);
|
||||
try out.append(allocator, '\n');
|
||||
}
|
||||
|
||||
test "regular: profile status makes unavailable surfaces explicit" {
|
||||
const text = try statusAlloc(std.testing.allocator);
|
||||
defer std.testing.allocator.free(text);
|
||||
try std.testing.expect(std.mem.indexOf(u8, text, "profile:v1\n") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, text, "socket=local_only") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, text, "remote_transport=not_built") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, text, "plugin_host=not_built") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, text, "tool_mutation=protocol_logged") != null);
|
||||
}
|
||||
@@ -20,6 +20,7 @@ const SocketError = error{
|
||||
ConnectFailed,
|
||||
WriteFailed,
|
||||
RequestTooLarge,
|
||||
NonLocalPath,
|
||||
};
|
||||
|
||||
pub const Server = struct {
|
||||
@@ -29,11 +30,14 @@ pub const Server = struct {
|
||||
session: *session_mod.Session,
|
||||
|
||||
pub fn listen(allocator: std.mem.Allocator, path: []const u8, session: *session_mod.Session) !Server {
|
||||
try validateLocalSocketPath(path);
|
||||
unlinkPath(path);
|
||||
const fd = try createSocket();
|
||||
errdefer closeFd(fd);
|
||||
|
||||
const addr = try unixAddress(path);
|
||||
const old_umask = restrictSocketUmask();
|
||||
defer restoreUmask(old_umask);
|
||||
if (std.posix.errno(std.posix.system.bind(fd, @ptrCast(&addr.un), addr.len)) != .SUCCESS) return SocketError.BindFailed;
|
||||
if (std.posix.errno(std.posix.system.listen(fd, 8)) != .SUCCESS) return SocketError.ListenFailed;
|
||||
|
||||
@@ -72,6 +76,7 @@ pub const Server = struct {
|
||||
};
|
||||
|
||||
pub fn request(allocator: std.mem.Allocator, path: []const u8, line: []const u8) ![]u8 {
|
||||
try validateLocalSocketPath(path);
|
||||
const fd = try createSocket();
|
||||
defer closeFd(fd);
|
||||
|
||||
@@ -104,6 +109,21 @@ const UnixAddress = struct {
|
||||
len: std.posix.socklen_t,
|
||||
};
|
||||
|
||||
pub fn validateLocalSocketPath(path: []const u8) !void {
|
||||
if (path.len == 0) return SocketError.NonLocalPath;
|
||||
if (std.mem.indexOfScalar(u8, path, 0) != null) return SocketError.NonLocalPath;
|
||||
if (std.mem.indexOfScalar(u8, path, '\n') != null or std.mem.indexOfScalar(u8, path, '\r') != null) return SocketError.NonLocalPath;
|
||||
if (!std.mem.startsWith(u8, path, "/tmp/") and !std.mem.startsWith(u8, path, "/var/tmp/")) return SocketError.NonLocalPath;
|
||||
}
|
||||
|
||||
fn restrictSocketUmask() usize {
|
||||
return std.os.linux.syscall1(.umask, 0o177);
|
||||
}
|
||||
|
||||
fn restoreUmask(old_umask: usize) void {
|
||||
_ = std.os.linux.syscall1(.umask, old_umask);
|
||||
}
|
||||
|
||||
fn unixAddress(path: []const u8) !UnixAddress {
|
||||
var addr: std.posix.sockaddr.un = .{
|
||||
.family = std.posix.AF.UNIX,
|
||||
@@ -161,6 +181,14 @@ fn unlinkPath(path: []const u8) void {
|
||||
_ = std.posix.system.unlink(&posix_path);
|
||||
}
|
||||
|
||||
test "regular: local socket path validation rejects non-local transports" {
|
||||
try validateLocalSocketPath("/tmp/mim.sock");
|
||||
try validateLocalSocketPath("/var/tmp/mim.sock");
|
||||
try std.testing.expectError(SocketError.NonLocalPath, validateLocalSocketPath("relative.sock"));
|
||||
try std.testing.expectError(SocketError.NonLocalPath, validateLocalSocketPath("/home/user/mim.sock"));
|
||||
try std.testing.expectError(SocketError.NonLocalPath, validateLocalSocketPath("/tmp/bad\n.sock"));
|
||||
}
|
||||
|
||||
test "regular: local socket state request observes a running session" {
|
||||
var session = session_mod.Session.init(std.testing.allocator);
|
||||
defer session.deinit();
|
||||
|
||||
Reference in New Issue
Block a user