Fence local profile capabilities

This commit is contained in:
slhx agent
2026-06-21 05:15:01 +02:00
parent 73c65a9b6f
commit fb68747848
3 changed files with 118 additions and 0 deletions
+39
View File
@@ -8,6 +8,7 @@ const lsp = @import("lsp.zig");
const mobile_acceptance = @import("mobile_acceptance.zig"); const mobile_acceptance = @import("mobile_acceptance.zig");
const panel = @import("panel.zig"); const panel = @import("panel.zig");
const pi_bridge = @import("pi_bridge.zig"); const pi_bridge = @import("pi_bridge.zig");
const profile = @import("profile.zig");
const protocol = @import("protocol.zig"); const protocol = @import("protocol.zig");
const replay = @import("replay.zig"); const replay = @import("replay.zig");
const repo = @import("repo.zig"); const repo = @import("repo.zig");
@@ -26,6 +27,7 @@ const help_text =
\\ mim [--help] \\ mim [--help]
\\ mim [--version] \\ mim [--version]
\\ mim context [task] [intent] \\ mim context [task] [intent]
\\ mim profile
\\ mim mimctl context <socket|-> [task] [intent] \\ mim mimctl context <socket|-> [task] [intent]
\\ mim --mimctl <socket|-> <request...> \\ mim --mimctl <socket|-> <request...>
\\ \\
@@ -71,6 +73,14 @@ pub fn main(init: std.process.Init) !u8 {
try printLocalContext(allocator, init.io, &args, stdout); try printLocalContext(allocator, init.io, &args, stdout);
return 0; 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")) { if (std.mem.eql(u8, arg, "mimctl")) {
const sub = args.next() orelse { const sub = args.next() orelse {
try stderr.writeStreamingAll(init.io, "mim: mimctl requires a subcommand\n"); 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); 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( fn requestMimctl(
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
io: std.Io, io: std.Io,
@@ -193,6 +223,7 @@ test {
_ = mobile_acceptance; _ = mobile_acceptance;
_ = panel; _ = panel;
_ = pi_bridge; _ = pi_bridge;
_ = profile;
_ = protocol; _ = protocol;
_ = replay; _ = replay;
_ = repo; _ = 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, "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, "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 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" { test "regular: version output is stable enough for smoke checks" {
+51
View File
@@ -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);
}
+28
View File
@@ -20,6 +20,7 @@ const SocketError = error{
ConnectFailed, ConnectFailed,
WriteFailed, WriteFailed,
RequestTooLarge, RequestTooLarge,
NonLocalPath,
}; };
pub const Server = struct { pub const Server = struct {
@@ -29,11 +30,14 @@ pub const Server = struct {
session: *session_mod.Session, session: *session_mod.Session,
pub fn listen(allocator: std.mem.Allocator, path: []const u8, session: *session_mod.Session) !Server { pub fn listen(allocator: std.mem.Allocator, path: []const u8, session: *session_mod.Session) !Server {
try validateLocalSocketPath(path);
unlinkPath(path); unlinkPath(path);
const fd = try createSocket(); const fd = try createSocket();
errdefer closeFd(fd); errdefer closeFd(fd);
const addr = try unixAddress(path); 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.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; 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 { pub fn request(allocator: std.mem.Allocator, path: []const u8, line: []const u8) ![]u8 {
try validateLocalSocketPath(path);
const fd = try createSocket(); const fd = try createSocket();
defer closeFd(fd); defer closeFd(fd);
@@ -104,6 +109,21 @@ const UnixAddress = struct {
len: std.posix.socklen_t, 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 { fn unixAddress(path: []const u8) !UnixAddress {
var addr: std.posix.sockaddr.un = .{ var addr: std.posix.sockaddr.un = .{
.family = std.posix.AF.UNIX, .family = std.posix.AF.UNIX,
@@ -161,6 +181,14 @@ fn unlinkPath(path: []const u8) void {
_ = std.posix.system.unlink(&posix_path); _ = 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" { test "regular: local socket state request observes a running session" {
var session = session_mod.Session.init(std.testing.allocator); var session = session_mod.Session.init(std.testing.allocator);
defer session.deinit(); defer session.deinit();