293 lines · 10.7 KB · Zig
RawDownload
const std = @import("std");const builtin = @import("builtin");const command_admission = @import("../permissions/command_admission.zig");const command_environment = @import("command_environment.zig");const command_effect = @import("../shell_command/command_effect.zig");const debug_trace = @import("../shared/debug_trace.zig");const local_executor = @import("local_executor.zig");const sandbox = @import("../permissions/sandbox.zig"); pub const RouteKind = local_executor.RouteKind;pub const PreparedCommandRoute = local_executor.PreparedCommand;pub const RoutedCommandResult = local_executor.CommandResult; /// Upper bound for raw callback bytes that can still be represented by the/// route's ordinary foreground result. Above this bound the route either/// rejects output or promotes it to an unordered command artifact.pub fn foregroundResultComparisonLimit(    route: PreparedCommandRoute,    approved_shell_limit: usize,) usize {    return local_executor.foregroundResultComparisonLimit(route, approved_shell_limit);} pub fn prepareAuthorizedRoute(    alloc: std.mem.Allocator,    command_ctx: command_admission.CommandContext,    authority: command_admission.CommandExecutionAuthority,) !PreparedCommandRoute {    if (command_ctx.target_os != builtin.os.tag) return error.CommandTargetMismatch;    if (sandbox.resolveBackend(command_ctx.resolved_backend) != command_ctx.resolved_backend) {        return error.CommandBackendNotResolved;    }     return switch (authority) {        .direct_only => |fingerprint| blk: {            if (!fingerprint.matches(command_ctx)) return error.CommandAdmissionChanged;            var admission = command_effect.plan(                alloc,                command_ctx.command,                command_ctx.resolved_cwd,                command_ctx.background,                command_ctx.resolved_backend,                command_ctx.target_os,            ) catch return error.CommandAdmissionChanged;            switch (admission) {                .direct_read_only => |plan| {                    debug_trace.logf("core", "terminal.exec authority=direct_only route=direct_read_only", .{});                    break :blk .{ .direct_read_only = plan };                },                .approval_required => {                    admission.deinit(alloc);                    return error.CommandAdmissionChanged;                },            }        },        .shell_allowed => |shell| blk: {            if (!shell.fingerprint.matches(command_ctx)) {                return error.CommandAuthorityContextMismatch;            }            if (command_ctx.environment.requiresShellRoute()) {                debug_trace.logf(                    "core",                    "terminal.exec authority=shell_allowed source={s} route=approved_shell environment={s}",                    .{                        @tagName(shell.source),                        @tagName(std.meta.activeTag(command_ctx.environment)),                    },                );                break :blk .{ .approved_shell = .{                    .command_ctx = command_ctx,                    .reason = .dynamic_shell,                    .source = shell.source,                } };            }            const admission = command_effect.plan(                alloc,                command_ctx.command,                command_ctx.resolved_cwd,                command_ctx.background,                command_ctx.resolved_backend,                command_ctx.target_os,            ) catch break :blk .{ .approved_shell = .{                .command_ctx = command_ctx,                .reason = .planning_failure,                .source = shell.source,            } };            switch (admission) {                .direct_read_only => |plan| {                    debug_trace.logf("core", "terminal.exec authority=shell_allowed source={s} route=direct_read_only", .{@tagName(shell.source)});                    break :blk .{ .direct_read_only = plan };                },                .approval_required => |reason| {                    debug_trace.logf("core", "terminal.exec authority=shell_allowed source={s} route=approved_shell reason={s}", .{ @tagName(shell.source), @tagName(reason) });                    break :blk .{ .approved_shell = .{                        .command_ctx = command_ctx,                        .reason = reason,                        .source = shell.source,                    } };                },            }        },    };} pub fn executePreparedRoute(    cfg: sandbox.Config,    alloc: std.mem.Allocator,    route: PreparedCommandRoute,) !RoutedCommandResult {    return local_executor.executePreparedCommand(cfg, alloc, route);} pub fn executePlannedCommand(    cfg: sandbox.Config,    alloc: std.mem.Allocator,    command_ctx: command_admission.CommandContext,    authority: command_admission.CommandExecutionAuthority,) !RoutedCommandResult {    try validateConfigContext(cfg, command_ctx);    const route = try prepareAuthorizedRoute(alloc, command_ctx, authority);    return executePreparedRoute(cfg, alloc, route);} pub fn validateConfigContext(    cfg: sandbox.Config,    command_ctx: command_admission.CommandContext,) !void {    if (sandbox.resolveBackend(cfg.backend) != command_ctx.resolved_backend) {        return error.CommandBackendMismatch;    }    if (command_ctx.target_os != builtin.os.tag) return error.CommandTargetMismatch;} fn context(command: []const u8, background: bool) command_admission.CommandContext {    return .{        .command = command,        .resolved_cwd = "/tmp",        .background = background,        .resolved_backend = .none,        .target_os = builtin.os.tag,    };} fn directAuthority(ctx: command_admission.CommandContext) command_admission.CommandExecutionAuthority {    return .{ .direct_only = .init(ctx) };} fn shellAuthority(    ctx: command_admission.CommandContext,    source: command_admission.ShellAuthorizationSource,) command_admission.CommandExecutionAuthority {    return .{ .shell_allowed = .{        .fingerprint = .init(ctx),        .source = source,    } };} test {    _ = @import("local_executor.zig");} test "router revalidates direct-only authority and rejects changed admission" {    const ctx = context("pwd", false);    var route = try prepareAuthorizedRoute(std.testing.allocator, ctx, directAuthority(ctx));    defer route.deinit(std.testing.allocator);    try std.testing.expect(route == .direct_read_only);     var changed = ctx;    changed.command = "touch changed";    try std.testing.expectError(        error.CommandAdmissionChanged,        prepareAuthorizedRoute(std.testing.allocator, changed, directAuthority(ctx)),    );} test "router keeps direct grammar on the safer route for every shell source" {    const ctx = context("pwd", false);    const sources = [_]command_admission.ShellAuthorizationSource{        .configured_rule,        .session_grant,        .js_host_workspace_sandbox,        .interactive_once,        .interactive_always,        .auto_mode,        .auto_classifier,        .yolo,    };    for (sources) |source| {        var route = try prepareAuthorizedRoute(            std.testing.allocator,            ctx,            shellAuthority(ctx, source),        );        defer route.deinit(std.testing.allocator);        try std.testing.expect(route == .direct_read_only);    }} test "router never sends explicit profiles through direct execution" {    for ([_]command_environment.Environment{        .{ .clean = "/bin/zsh" },        .{ .user = "/bin/zsh" },    }) |environment| {        var ctx = context("ls -l", false);        ctx.environment = environment;        var route = try prepareAuthorizedRoute(            std.testing.allocator,            ctx,            shellAuthority(ctx, .interactive_once),        );        defer route.deinit(std.testing.allocator);        try std.testing.expect(route == .approved_shell);        try std.testing.expectEqual(            command_effect.ApprovalReason.dynamic_shell,            route.approved_shell.reason,        );    }} test "router permits approval-bearing grammar only with sourced shell authority" {    const ctx = context("touch created.txt", false);    try std.testing.expectError(        error.CommandAdmissionChanged,        prepareAuthorizedRoute(std.testing.allocator, ctx, directAuthority(ctx)),    );     var route = try prepareAuthorizedRoute(        std.testing.allocator,        ctx,        shellAuthority(ctx, .configured_rule),    );    defer route.deinit(std.testing.allocator);    try std.testing.expect(route == .approved_shell);    try std.testing.expectEqual(        @as(usize, 1024),        foregroundResultComparisonLimit(route, 1024),    );    try std.testing.expectEqual(command_effect.ApprovalReason.filesystem_write, route.approved_shell.reason);    try std.testing.expectEqual(        command_admission.ShellAuthorizationSource.configured_rule,        route.approved_shell.source,    );} test "router rejects shell fingerprint backend and target mismatches" {    const ctx = context("git status", false);    var changed = ctx;    changed.resolved_cwd = "/other";    try std.testing.expectError(        error.CommandAuthorityContextMismatch,        prepareAuthorizedRoute(            std.testing.allocator,            changed,            shellAuthority(ctx, .interactive_once),        ),    );     const cfg = sandbox.Config{        .backend = .macos,        .workspace_root = "/tmp",        .max_command_output_bytes = 1024,    };    try std.testing.expectError(error.CommandBackendMismatch, validateConfigContext(cfg, ctx));     var changed_environment = ctx;    changed_environment.environment = .{ .clean = "/bin/zsh" };    try std.testing.expectError(        error.CommandAuthorityContextMismatch,        prepareAuthorizedRoute(            std.testing.allocator,            changed_environment,            shellAuthority(ctx, .interactive_once),        ),    );} test "router executes direct plan without consulting approved shell capacity" {    var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);    defer arena_state.deinit();    const arena = arena_state.allocator();    const ctx = context("printf x | wc -c", false);    const routed = try executePlannedCommand(.{        .backend = .none,        .workspace_root = "/tmp",        .max_command_output_bytes = 1,    }, arena, ctx, directAuthority(ctx));    try std.testing.expectEqual(RouteKind.direct_read_only, routed.route);    try std.testing.expect(std.mem.find(u8, routed.result.output, "<stdout>\n1\n</stdout>") != null);    try std.testing.expectEqual(        @as(?[]const u8, null),        routed.result.command_result.?.foreground.output_file,    );}