const std = @import("std");const server_detection = @import("../../core/background/server_detection.zig");const io_mod = @import("../../core/shared/io.zig");const tool_dispatch = @import("../../core/tooling/tool_dispatch.zig");const pathing = @import("../../core/workspace/pathing.zig");const workspace_access = @import("../../core/workspace/workspace_access.zig"); const Allocator = std.mem.Allocator; /// Typed input for the core run_command tool.pub const Input = struct { command: []u8, cwd: ?[]u8 = null, background: bool = false, /// Frees owned command and optional cwd strings. pub fn deinit(self: *Input, alloc: Allocator) void { alloc.free(self.command); if (self.cwd) |cwd| alloc.free(cwd); self.* = .{ .command = &.{} }; }}; /// Decodes run_command JSON into an owned Input released by ToolInput.deinit.pub fn decode(ctx: tool_dispatch.DispatchContext, args_json: []const u8) tool_dispatch.DispatchError!tool_dispatch.DecodeResult { var parsed = std.json.parseFromSlice(std.json.Value, ctx.allocator, args_json, .{}) catch { return .{ .failure = try ctx.allocator.dupe(u8, "run_command arguments must be valid JSON") }; }; defer parsed.deinit(); if (parsed.value != .object) { return .{ .failure = try ctx.allocator.dupe(u8, "run_command arguments must be an object") }; } const command_value = parsed.value.object.get("command") orelse { return .{ .failure = try ctx.allocator.dupe(u8, "run_command requires string field \"command\"") }; }; if (command_value != .string) { return .{ .failure = try ctx.allocator.dupe(u8, "run_command field \"command\" must be a string") }; } const cwd_raw: ?[]const u8 = if (parsed.value.object.get("cwd")) |cwd_value| blk: { if (cwd_value != .string) { return .{ .failure = try ctx.allocator.dupe(u8, "run_command field \"cwd\" must be a string") }; } break :blk cwd_value.string; } else null; var run_in_background = false; if (parsed.value.object.get("background")) |background_value| { if (background_value != .bool) { return .{ .failure = try ctx.allocator.dupe(u8, "run_command field \"background\" must be a bool") }; } run_in_background = background_value.bool; } const command = try ctx.allocator.dupe(u8, command_value.string); errdefer ctx.allocator.free(command); const cwd: ?[]u8 = if (cwd_raw) |raw| try ctx.allocator.dupe(u8, raw) else null; errdefer if (cwd) |owned| ctx.allocator.free(owned); const input = try ctx.allocator.create(Input); errdefer ctx.allocator.destroy(input); input.* = .{ .command = command, .cwd = cwd, .background = run_in_background, }; return .{ .input = .{ .ptr = input, .deinit_fn = inputDeinit } };} fn inputDeinit(ptr: *anyopaque, alloc: Allocator) void { const input: *Input = @ptrCast(@alignCast(ptr)); input.deinit(alloc); alloc.destroy(input);} /// Leaves the active run_command public contract to the runtime.pub fn validate(_: tool_dispatch.DispatchContext, _: tool_dispatch.ToolInput) tool_dispatch.DispatchError!?[]u8 { return null;} /// Resolves the command cwd using the same rule as the runner and permission target.pub fn resolveCwd(arena: Allocator, scope: workspace_access.AccessScope, input: *const Input) ![]const u8 { const cwd_input = input.cwd orelse return arena.dupe(u8, scope.primary_directory); if (std.mem.eql(u8, cwd_input, ".")) return arena.dupe(u8, scope.primary_directory); return pathing.resolveWorkspaceOrExternalPath(arena, scope.primary_directory, cwd_input);} /// Delegates validated input to the Core-owned command execution backend.pub fn call(ctx: tool_dispatch.DispatchContext, erased: tool_dispatch.ToolInput) tool_dispatch.DispatchError!tool_dispatch.ToolResult { const backend = ctx.run_command_backend orelse return .{ .failure = try ctx.allocator.dupe(u8, "run_command execution backend is unavailable\n"), }; const input = erased.as(Input); const cwd = resolveCwd( ctx.allocator, ctx.access_scope orelse workspace_access.AccessScope.primaryOnly(ctx.workspace_root), input, ) catch |err| { if (err == error.OutOfMemory) return error.OutOfMemory; return .{ .failure = try std.fmt.allocPrint(ctx.allocator, "Unable to resolve command cwd: {s}", .{@errorName(err)}) }; }; defer ctx.allocator.free(@constCast(cwd)); return backend.execute(ctx, .{ .command = input.command, .resolved_cwd = cwd, .background = input.background, .expects_url = server_detection.isLikelyServerCommand(input.command), });} /// Reports that run_command may mutate process or filesystem state.pub fn readsOnly(_: tool_dispatch.ToolInput) bool { return false;} /// Reports that run_command is not intrinsically irreversible.pub fn isIrreversible(_: tool_dispatch.ToolInput) bool { return false;} fn noopInputDeinit(_: *anyopaque, _: Allocator) void {} fn stackInput(input: *Input) tool_dispatch.ToolInput { return .{ .ptr = input, .deinit_fn = noopInputDeinit };} fn validateStackInput(alloc: Allocator, input: *Input) !?[]u8 { return validate(.{ .allocator = alloc }, stackInput(input));} fn decodeInput(args_json: []const u8) !tool_dispatch.ToolInput { const decoded = try decode(.{ .allocator = std.testing.allocator }, args_json); return switch (decoded) { .input => |input| input, .failure => |body| { defer std.testing.allocator.free(body); return error.TestUnexpectedDecodeFailure; }, };} fn expectDecodeFailure(args_json: []const u8, expected: []const u8) !void { const decoded = try decode(.{ .allocator = std.testing.allocator }, args_json); switch (decoded) { .input => |input| { input.deinit(std.testing.allocator); return error.TestExpectedDecodeFailure; }, .failure => |body| { defer std.testing.allocator.free(body); try std.testing.expectEqualStrings(expected, body); }, }} test "decode accepts active fields and ignores unknown keys" { const erased = try decodeInput("{\"command\":\"echo ok\",\"cwd\":\"src\",\"background\":true,\"timeout_ms\":500,\"extra\":1}"); defer erased.deinit(std.testing.allocator); const input = erased.as(Input); try std.testing.expectEqualStrings("echo ok", input.command); try std.testing.expectEqualStrings("src", input.cwd.?); try std.testing.expect(input.background);} test "decode frees valid cwd when background has invalid type" { try expectDecodeFailure( "{\"command\":\"echo ok\",\"cwd\":\"src\",\"background\":\"yes\"}", "run_command field \"background\" must be a bool", );} test "decode ignores unsigned timeout_ms type" { const erased = try decodeInput("{\"command\":\"echo ok\",\"cwd\":\"src\",\"timeout_ms\":\"1000\"}"); defer erased.deinit(std.testing.allocator); const input = erased.as(Input); try std.testing.expectEqualStrings("echo ok", input.command); try std.testing.expectEqualStrings("src", input.cwd.?);} test "validation preserves command bytes without public trimming" { var empty = Input{ .command = try std.testing.allocator.dupe(u8, "") }; defer empty.deinit(std.testing.allocator); try std.testing.expect((try validateStackInput(std.testing.allocator, &empty)) == null); try std.testing.expectEqualStrings("", empty.command); var whitespace_input = Input{ .command = try std.testing.allocator.dupe(u8, " \t\n ") }; defer whitespace_input.deinit(std.testing.allocator); try std.testing.expect((try validateStackInput(std.testing.allocator, &whitespace_input)) == null); try std.testing.expectEqualStrings(" \t\n ", whitespace_input.command);} test "cwd resolution handles default and non default" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(io_mod.getIo(), "workspace/src"); const workspace = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "workspace"); defer std.testing.allocator.free(workspace); const expected = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "workspace/src"); defer std.testing.allocator.free(expected); var default_input = Input{ .command = try std.testing.allocator.dupe(u8, "echo") }; defer default_input.deinit(std.testing.allocator); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const scope = workspace_access.AccessScope.primaryOnly(workspace); try std.testing.expectEqualStrings(workspace, try resolveCwd(arena_state.allocator(), scope, &default_input)); var src_input = Input{ .command = try std.testing.allocator.dupe(u8, "echo"), .cwd = try std.testing.allocator.dupe(u8, "src"), }; defer src_input.deinit(std.testing.allocator); try std.testing.expectEqualStrings(expected, try resolveCwd(arena_state.allocator(), scope, &src_input));} test "cwd resolution accepts external absolute and workspace escape paths" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); try tmp.dir.createDirPath(io_mod.getIo(), "shared"); const workspace = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "workspace"); defer std.testing.allocator.free(workspace); const shared = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "shared"); defer std.testing.allocator.free(shared); const entries = [_]workspace_access.Entry{.{ .path = @constCast(shared), .saved = true, .command_line = false, .available = true, .active = true, }}; const scope = workspace_access.AccessScope{ .primary_directory = workspace, .additional_directories = &entries, }; var input = Input{ .command = try std.testing.allocator.dupe(u8, "pwd"), .cwd = try std.testing.allocator.dupe(u8, shared), }; defer input.deinit(std.testing.allocator); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); try std.testing.expectEqualStrings(shared, try resolveCwd(arena_state.allocator(), scope, &input)); try std.testing.expectEqualStrings( shared, try resolveCwd(arena_state.allocator(), workspace_access.AccessScope.primaryOnly(workspace), &input), ); var relative_input = Input{ .command = try std.testing.allocator.dupe(u8, "pwd"), .cwd = try std.testing.allocator.dupe(u8, "../shared"), }; defer relative_input.deinit(std.testing.allocator); try std.testing.expectEqualStrings( shared, try resolveCwd(arena_state.allocator(), workspace_access.AccessScope.primaryOnly(workspace), &relative_input), );} const FakeBackend = struct { called: bool = false, command: []const u8 = "", expects_url: bool = false, fn execute( maybe_ctx: ?*anyopaque, ctx: tool_dispatch.DispatchContext, request: tool_dispatch.RunCommandRequest, ) tool_dispatch.DispatchError!tool_dispatch.ToolResult { const raw_ctx = maybe_ctx orelse unreachable; const self: *FakeBackend = @ptrCast(@alignCast(raw_ctx)); self.called = true; self.command = request.command; self.expects_url = request.expects_url; return .{ .success = try ctx.allocator.dupe(u8, "backend result") }; }}; test "registered callback delegates typed input to core backend" { const erased = try decodeInput("{\"command\":\"npm run dev\"}"); defer erased.deinit(std.testing.allocator); var backend = FakeBackend{}; var result = try call(.{ .allocator = std.testing.allocator, .run_command_backend = .{ .ctx = &backend, .execute_fn = FakeBackend.execute, }, }, erased); defer result.deinit(std.testing.allocator); try std.testing.expect(backend.called); try std.testing.expectEqualStrings("npm run dev", backend.command); try std.testing.expect(backend.expects_url); try std.testing.expectEqualStrings("backend result", result.success);} test "registered callback fails when core backend is unavailable" { const erased = try decodeInput("{\"command\":\"printf unavailable\"}"); defer erased.deinit(std.testing.allocator); var result = try call(.{ .allocator = std.testing.allocator }, erased); defer result.deinit(std.testing.allocator); try std.testing.expectEqualStrings( "run_command execution backend is unavailable\n", result.failure, );}
const std = @import("std");
const server_detection = @import("../../core/background/server_detection.zig");
const io_mod = @import("../../core/shared/io.zig");
const tool_dispatch = @import("../../core/tooling/tool_dispatch.zig");
const pathing = @import("../../core/workspace/pathing.zig");
const workspace_access = @import("../../core/workspace/workspace_access.zig");
const Allocator = std.mem.Allocator;
/// Typed input for the core run_command tool.
pub const Input = struct {
command: []u8,
cwd: ?[]u8 = null,
background: bool = false,
/// Frees owned command and optional cwd strings.
pub fn deinit(self: *Input, alloc: Allocator) void {
alloc.free(self.command);
if (self.cwd) |cwd| alloc.free(cwd);
self.* = .{ .command = &.{} };
}
};
/// Decodes run_command JSON into an owned Input released by ToolInput.deinit.
pub fn decode(ctx: tool_dispatch.DispatchContext, args_json: []const u8) tool_dispatch.DispatchError!tool_dispatch.DecodeResult {
var parsed = std.json.parseFromSlice(std.json.Value, ctx.allocator, args_json, .{}) catch {
return .{ .failure = try ctx.allocator.dupe(u8, "run_command arguments must be valid JSON") };
};
defer parsed.deinit();
if (parsed.value != .object) {
return .{ .failure = try ctx.allocator.dupe(u8, "run_command arguments must be an object") };
}
const command_value = parsed.value.object.get("command") orelse {
return .{ .failure = try ctx.allocator.dupe(u8, "run_command requires string field \"command\"") };
};
if (command_value != .string) {
return .{ .failure = try ctx.allocator.dupe(u8, "run_command field \"command\" must be a string") };
}
const cwd_raw: ?[]const u8 = if (parsed.value.object.get("cwd")) |cwd_value| blk: {
if (cwd_value != .string) {
return .{ .failure = try ctx.allocator.dupe(u8, "run_command field \"cwd\" must be a string") };
}
break :blk cwd_value.string;
} else null;
var run_in_background = false;
if (parsed.value.object.get("background")) |background_value| {
if (background_value != .bool) {
return .{ .failure = try ctx.allocator.dupe(u8, "run_command field \"background\" must be a bool") };
}
run_in_background = background_value.bool;
}
const command = try ctx.allocator.dupe(u8, command_value.string);
errdefer ctx.allocator.free(command);
const cwd: ?[]u8 = if (cwd_raw) |raw| try ctx.allocator.dupe(u8, raw) else null;
errdefer if (cwd) |owned| ctx.allocator.free(owned);
const input = try ctx.allocator.create(Input);
errdefer ctx.allocator.destroy(input);
input.* = .{
.command = command,
.cwd = cwd,
.background = run_in_background,
};
return .{ .input = .{ .ptr = input, .deinit_fn = inputDeinit } };
}
fn inputDeinit(ptr: *anyopaque, alloc: Allocator) void {
const input: *Input = @ptrCast(@alignCast(ptr));
input.deinit(alloc);
alloc.destroy(input);
}
/// Leaves the active run_command public contract to the runtime.
pub fn validate(_: tool_dispatch.DispatchContext, _: tool_dispatch.ToolInput) tool_dispatch.DispatchError!?[]u8 {
return null;
}
/// Resolves the command cwd using the same rule as the runner and permission target.
pub fn resolveCwd(arena: Allocator, scope: workspace_access.AccessScope, input: *const Input) ![]const u8 {
const cwd_input = input.cwd orelse return arena.dupe(u8, scope.primary_directory);
if (std.mem.eql(u8, cwd_input, ".")) return arena.dupe(u8, scope.primary_directory);
return pathing.resolveWorkspaceOrExternalPath(arena, scope.primary_directory, cwd_input);
}
/// Delegates validated input to the Core-owned command execution backend.
pub fn call(ctx: tool_dispatch.DispatchContext, erased: tool_dispatch.ToolInput) tool_dispatch.DispatchError!tool_dispatch.ToolResult {
const backend = ctx.run_command_backend orelse return .{
.failure = try ctx.allocator.dupe(u8, "run_command execution backend is unavailable\n"),
};
const input = erased.as(Input);
const cwd = resolveCwd(
ctx.allocator,
ctx.access_scope orelse workspace_access.AccessScope.primaryOnly(ctx.workspace_root),
input,
) catch |err| {
if (err == error.OutOfMemory) return error.OutOfMemory;
return .{ .failure = try std.fmt.allocPrint(ctx.allocator, "Unable to resolve command cwd: {s}", .{@errorName(err)}) };
};
defer ctx.allocator.free(@constCast(cwd));
return backend.execute(ctx, .{
.command = input.command,
.resolved_cwd = cwd,
.background = input.background,
.expects_url = server_detection.isLikelyServerCommand(input.command),
});
}
/// Reports that run_command may mutate process or filesystem state.
pub fn readsOnly(_: tool_dispatch.ToolInput) bool {
return false;
}
/// Reports that run_command is not intrinsically irreversible.
pub fn isIrreversible(_: tool_dispatch.ToolInput) bool {
return false;
}
fn noopInputDeinit(_: *anyopaque, _: Allocator) void {}
fn stackInput(input: *Input) tool_dispatch.ToolInput {
return .{ .ptr = input, .deinit_fn = noopInputDeinit };
}
fn validateStackInput(alloc: Allocator, input: *Input) !?[]u8 {
return validate(.{ .allocator = alloc }, stackInput(input));
}
fn decodeInput(args_json: []const u8) !tool_dispatch.ToolInput {
const decoded = try decode(.{ .allocator = std.testing.allocator }, args_json);
return switch (decoded) {
.input => |input| input,
.failure => |body| {
defer std.testing.allocator.free(body);
return error.TestUnexpectedDecodeFailure;
},
};
}
fn expectDecodeFailure(args_json: []const u8, expected: []const u8) !void {
const decoded = try decode(.{ .allocator = std.testing.allocator }, args_json);
switch (decoded) {
.input => |input| {
input.deinit(std.testing.allocator);
return error.TestExpectedDecodeFailure;
},
.failure => |body| {
defer std.testing.allocator.free(body);
try std.testing.expectEqualStrings(expected, body);
},
}
}
test "decode accepts active fields and ignores unknown keys" {
const erased = try decodeInput("{\"command\":\"echo ok\",\"cwd\":\"src\",\"background\":true,\"timeout_ms\":500,\"extra\":1}");
defer erased.deinit(std.testing.allocator);
const input = erased.as(Input);
try std.testing.expectEqualStrings("echo ok", input.command);
try std.testing.expectEqualStrings("src", input.cwd.?);
try std.testing.expect(input.background);
}
test "decode frees valid cwd when background has invalid type" {
try expectDecodeFailure(
"{\"command\":\"echo ok\",\"cwd\":\"src\",\"background\":\"yes\"}",
"run_command field \"background\" must be a bool",
);
}
test "decode ignores unsigned timeout_ms type" {
const erased = try decodeInput("{\"command\":\"echo ok\",\"cwd\":\"src\",\"timeout_ms\":\"1000\"}");
defer erased.deinit(std.testing.allocator);
const input = erased.as(Input);
try std.testing.expectEqualStrings("echo ok", input.command);
try std.testing.expectEqualStrings("src", input.cwd.?);
}
test "validation preserves command bytes without public trimming" {
var empty = Input{ .command = try std.testing.allocator.dupe(u8, "") };
defer empty.deinit(std.testing.allocator);
try std.testing.expect((try validateStackInput(std.testing.allocator, &empty)) == null);
try std.testing.expectEqualStrings("", empty.command);
var whitespace_input = Input{ .command = try std.testing.allocator.dupe(u8, " \t\n ") };
defer whitespace_input.deinit(std.testing.allocator);
try std.testing.expect((try validateStackInput(std.testing.allocator, &whitespace_input)) == null);
try std.testing.expectEqualStrings(" \t\n ", whitespace_input.command);
}
test "cwd resolution handles default and non default" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
try tmp.dir.createDirPath(io_mod.getIo(), "workspace/src");
const workspace = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "workspace");
defer std.testing.allocator.free(workspace);
const expected = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "workspace/src");
defer std.testing.allocator.free(expected);
var default_input = Input{ .command = try std.testing.allocator.dupe(u8, "echo") };
defer default_input.deinit(std.testing.allocator);
var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena_state.deinit();
const scope = workspace_access.AccessScope.primaryOnly(workspace);
try std.testing.expectEqualStrings(workspace, try resolveCwd(arena_state.allocator(), scope, &default_input));
var src_input = Input{
.command = try std.testing.allocator.dupe(u8, "echo"),
.cwd = try std.testing.allocator.dupe(u8, "src"),
};
defer src_input.deinit(std.testing.allocator);
try std.testing.expectEqualStrings(expected, try resolveCwd(arena_state.allocator(), scope, &src_input));
}
test "cwd resolution accepts external absolute and workspace escape paths" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
try tmp.dir.createDirPath(io_mod.getIo(), "workspace");
try tmp.dir.createDirPath(io_mod.getIo(), "shared");
const workspace = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "workspace");
defer std.testing.allocator.free(workspace);
const shared = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "shared");
defer std.testing.allocator.free(shared);
const entries = [_]workspace_access.Entry{.{
.path = @constCast(shared),
.saved = true,
.command_line = false,
.available = true,
.active = true,
}};
const scope = workspace_access.AccessScope{
.primary_directory = workspace,
.additional_directories = &entries,
};
var input = Input{
.command = try std.testing.allocator.dupe(u8, "pwd"),
.cwd = try std.testing.allocator.dupe(u8, shared),
};
defer input.deinit(std.testing.allocator);
var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena_state.deinit();
try std.testing.expectEqualStrings(shared, try resolveCwd(arena_state.allocator(), scope, &input));
try std.testing.expectEqualStrings(
shared,
try resolveCwd(arena_state.allocator(), workspace_access.AccessScope.primaryOnly(workspace), &input),
);
var relative_input = Input{
.command = try std.testing.allocator.dupe(u8, "pwd"),
.cwd = try std.testing.allocator.dupe(u8, "../shared"),
};
defer relative_input.deinit(std.testing.allocator);
try std.testing.expectEqualStrings(
shared,
try resolveCwd(arena_state.allocator(), workspace_access.AccessScope.primaryOnly(workspace), &relative_input),
);
}
const FakeBackend = struct {
called: bool = false,
command: []const u8 = "",
expects_url: bool = false,
fn execute(
maybe_ctx: ?*anyopaque,
ctx: tool_dispatch.DispatchContext,
request: tool_dispatch.RunCommandRequest,
) tool_dispatch.DispatchError!tool_dispatch.ToolResult {
const raw_ctx = maybe_ctx orelse unreachable;
const self: *FakeBackend = @ptrCast(@alignCast(raw_ctx));
self.called = true;
self.command = request.command;
self.expects_url = request.expects_url;
return .{ .success = try ctx.allocator.dupe(u8, "backend result") };
}
};
test "registered callback delegates typed input to core backend" {
const erased = try decodeInput("{\"command\":\"npm run dev\"}");
defer erased.deinit(std.testing.allocator);
var backend = FakeBackend{};
var result = try call(.{
.allocator = std.testing.allocator,
.run_command_backend = .{
.ctx = &backend,
.execute_fn = FakeBackend.execute,
},
}, erased);
defer result.deinit(std.testing.allocator);
try std.testing.expect(backend.called);
try std.testing.expectEqualStrings("npm run dev", backend.command);
try std.testing.expect(backend.expects_url);
try std.testing.expectEqualStrings("backend result", result.success);
}
test "registered callback fails when core backend is unavailable" {
const erased = try decodeInput("{\"command\":\"printf unavailable\"}");
defer erased.deinit(std.testing.allocator);
var result = try call(.{ .allocator = std.testing.allocator }, erased);
defer result.deinit(std.testing.allocator);
try std.testing.expectEqualStrings(
"run_command execution backend is unavailable\n",
result.failure,
);
}