892 lines · 31.8 KB · Zig
RawDownload
const std = @import("std");const auth_runtime = @import("../auth/auth_runtime.zig");const debug_trace = @import("../shared/debug_trace.zig");const io_mod = @import("../shared/io.zig");const agent_steps = @import("../config/agent_steps.zig");const config_runtime = @import("../config/config_runtime.zig");const host = @import("../hosts/host.zig");const mcp_contract = @import("../mcp/mcp_contract.zig");const sandbox = @import("../permissions/sandbox.zig");const session_store = @import("../session/session_store.zig");const types = @import("../shared/types.zig"); const Allocator = std.mem.Allocator;const default_session_diagnostics_limit: usize = 64; pub const CheckStatus = enum {    ok,    warn,    fail,}; pub const Check = struct {    name: []const u8,    status: CheckStatus,    detail: []const u8,    owned_detail: ?[]u8 = null,     pub fn deinit(self: *Check, alloc: Allocator) void {        if (self.owned_detail) |detail| alloc.free(detail);        self.* = undefined;    }}; pub const Snapshot = struct {    workspace_root: []u8,    model: []const u8,    owned_model: ?[]u8 = null,    auth: auth_runtime.StatusSnapshot = .{},    permission_mode: types.PermissionMode,    agent_step_limit: usize,    checks: []Check,     pub fn deinit(self: *Snapshot, alloc: Allocator) void {        alloc.free(self.workspace_root);        if (self.owned_model) |model| alloc.free(model);        self.auth.deinit(alloc);        for (self.checks) |*entry| entry.deinit(alloc);        alloc.free(self.checks);        self.* = undefined;    }}; const ResolvedModel = struct {    value: []const u8,    owned: ?[]u8 = null,}; pub fn collect(    alloc: Allocator,    secret_store: host.SecretStore,    default_model: []const u8,    default_agent_step_limit: usize,    mcp_config_diagnostic: mcp_contract.ProfileConfigDiagnostic,) !Snapshot {    const workspace_root = try io_mod.realpathAlloc(alloc, ".");    debug_trace.configureFromEnv(alloc, workspace_root);     var checks: std.ArrayList(Check) = .empty;    errdefer {        for (checks.items) |*entry| entry.deinit(alloc);        checks.deinit(alloc);    }     var snapshot = Snapshot{        .workspace_root = workspace_root,        .model = default_model,        .permission_mode = config_runtime.default_permission_mode,        .agent_step_limit = default_agent_step_limit,        .checks = &.{},    };    errdefer {        alloc.free(snapshot.workspace_root);        if (snapshot.owned_model) |model| alloc.free(model);        snapshot.auth.deinit(alloc);    }     const workspace_detail = try std.fmt.allocPrint(alloc, "using workspace {s}", .{snapshot.workspace_root});    try appendCheckOwned(&checks, alloc, "workspace", .ok, workspace_detail);     const paths = try config_runtime.discoverPaths(alloc, snapshot.workspace_root);    defer {        var mutable_paths = paths;        mutable_paths.deinit(alloc);    }     var detailed = config_runtime.loadMergedSettingsDetailed(alloc, snapshot.workspace_root) catch |err| {        // Settings are unreadable, so no remembered choice is available to honour.        snapshot.auth = try auth_runtime.loadStatusSnapshot(alloc, secret_store, null);        try appendConfigLoadFailureCheck(&checks, alloc, "config", "failed to load config", err);        try appendMcpConfigCheck(&checks, alloc, mcp_config_diagnostic);        try appendAuthCheck(&checks, alloc, snapshot.auth);        try appendConfigLoadFailureCheck(&checks, alloc, "startup", "failed to resolve startup settings", err);        try appendStateChecks(&checks, alloc, snapshot.workspace_root);        try appendGitCheck(&checks, alloc, snapshot.workspace_root);        try appendGhCheck(&checks, alloc);         snapshot.checks = try checks.toOwnedSlice(alloc);        return snapshot;    };    defer detailed.deinit(alloc);     snapshot.auth = try auth_runtime.loadStatusSnapshot(        alloc,        secret_store,        detailed.settings.credential_source,    );     try appendConfigCheck(&checks, alloc, paths, detailed.diagnostics);    try appendConfigDiagnosticChecks(&checks, alloc, detailed.diagnostics);    try appendMcpConfigCheck(&checks, alloc, mcp_config_diagnostic);    try appendAuthCheck(&checks, alloc, snapshot.auth);    try appendResolvedStartupCheck(&snapshot, &checks, alloc, .{        .model = detailed.settings.model,        .permission_mode = detailed.settings.permission_mode,        .max_agent_steps = detailed.settings.max_agent_steps,    }, default_model, default_agent_step_limit);    try appendStateChecks(&checks, alloc, snapshot.workspace_root);    try appendGitCheck(&checks, alloc, snapshot.workspace_root);    try appendGhCheck(&checks, alloc);     snapshot.checks = try checks.toOwnedSlice(alloc);    return snapshot;} fn appendConfigDiagnosticChecks(    checks: *std.ArrayList(Check),    alloc: Allocator,    diagnostics: []const config_runtime.ConfigDiagnostic,) !void {    for (diagnostics) |diagnostic| {        var detail_writer: std.Io.Writer.Allocating = .init(alloc);        defer detail_writer.deinit();        try detail_writer.writer.print(            "{s} config diagnostic: {s}",            .{ @tagName(diagnostic.layer), @tagName(diagnostic.cause) },        );        try config_runtime.writeDiagnosticMetadata(&detail_writer.writer, diagnostic);        const detail = try detail_writer.toOwnedSlice();        try appendCheckOwned(checks, alloc, "config", .warn, detail);    }} fn appendConfigCheck(    checks: *std.ArrayList(Check),    alloc: Allocator,    paths: config_runtime.Paths,    diagnostics: []const config_runtime.ConfigDiagnostic,) !void {    const user_exists = if (paths.user_settings) |path|        try fileExists(path) and !configLayerRejected(diagnostics, .user)    else        false;    const repo_exists = try fileExists(paths.workspace_settings) and        !configLayerRejected(diagnostics, .project);     if (!user_exists and !repo_exists) {        try appendCheck(checks, alloc, "config", .warn, "no config files found; using defaults and env overrides");        return;    }     const detail = try formatConfigPresence(alloc, user_exists, repo_exists);    try appendCheckOwned(checks, alloc, "config", .ok, detail);} fn configLayerRejected(    diagnostics: []const config_runtime.ConfigDiagnostic,    layer: config_runtime.ConfigLayer,) bool {    for (diagnostics) |diagnostic| {        if (diagnostic.layer != layer) continue;        switch (diagnostic.cause) {            .malformed_settings,            .settings_too_large,            .durable_path_unsafe,            .private_state_permissions_unsupported,            .invalid_model_id,            .retired_skill_match_fuzzy,            .invalid_context_limits,            => return true,            .invalid_additional_directories,            .ignored_project_user_only_setting,            .legacy_workspace_preferences,            .manual_backup_available,            => {},        }    }    return false;} fn appendAuthCheck(checks: *std.ArrayList(Check), alloc: Allocator, auth: auth_runtime.StatusSnapshot) !void {    if (auth.missingHelp(.cli)) |help| {        try appendCheck(checks, alloc, "auth", .fail, help);        return;    }     const detail = try auth.formatDoctorDetail(alloc);    try appendCheckOwned(checks, alloc, "auth", if (auth.expired) .warn else .ok, detail);} fn appendResolvedStartupCheck(    snapshot: *Snapshot,    checks: *std.ArrayList(Check),    alloc: Allocator,    settings: config_runtime.StartupStatusSettings,    default_model: []const u8,    default_agent_step_limit: usize,) !void {    const next_model = try resolveModel(alloc, default_model, settings.model);    if (snapshot.owned_model) |model| alloc.free(model);    snapshot.model = next_model.value;    snapshot.owned_model = next_model.owned;    snapshot.permission_mode = try resolvePermissionMode(settings.permission_mode);    snapshot.agent_step_limit = try resolveAgentStepLimit(default_agent_step_limit, settings.max_agent_steps);     const detail = try std.fmt.allocPrint(        alloc,        "resolved model={s}, permission_mode={s}, agent_step_limit={d}",        .{ snapshot.model, permissionModeLabel(snapshot.permission_mode), snapshot.agent_step_limit },    );    try appendCheckOwned(checks, alloc, "startup", .ok, detail);} fn appendConfigLoadFailureCheck(checks: *std.ArrayList(Check), alloc: Allocator, name: []const u8, prefix: []const u8, err: anyerror) !void {    const detail = try formatConfigLoadFailure(alloc, prefix, err);    try appendCheckOwned(checks, alloc, name, .fail, detail);} fn formatConfigLoadFailure(alloc: Allocator, prefix: []const u8, err: anyerror) ![]u8 {    return std.fmt.allocPrint(alloc, "{s}: {s}", .{ prefix, sandbox.configErrorMessage(err) orelse @errorName(err) });} fn appendStateChecks(checks: *std.ArrayList(Check), alloc: Allocator, workspace_root: []const u8) !void {    var store = session_store.Store.initReadOnly(alloc, workspace_root) catch |err| {        const status: CheckStatus = if (err == error.HomeNotSet) .warn else .fail;        const detail = try std.fmt.allocPrint(alloc, "failed to inspect workspace state: {s}", .{@errorName(err)});        try appendCheckOwned(checks, alloc, "state", status, detail);        return;    };    defer store.deinit(alloc);     if (store.canonical_root.sessions == null) {        try appendCheck(checks, alloc, "state", .warn, "durable state is not initialized");        try appendCheck(checks, alloc, "sessions", .warn, "no saved sessions yet");        return;    }     const state_detail = try std.fmt.allocPrint(alloc, "state dir ready at {s} (per-session managed state created on demand)", .{store.sessions_dir});    try appendCheckOwned(checks, alloc, "state", .ok, state_detail);     var inspection = try store.inspectForDoctorBounded(alloc, default_session_diagnostics_limit);    defer inspection.deinit(alloc);    try appendSessionDiagnosticChecks(        checks,        alloc,        &store,        inspection.diagnostics.items,    );    if (inspection.truncated) {        try appendSessionDiagnosticsTruncatedCheck(checks, alloc, inspection.inspected_count);    }     if (try store.readStateSummary(alloc)) |summary_value| {        var summary = summary_value;        defer summary.deinit(alloc);        try appendSessionsCountCheck(checks, alloc, summary.count, summary.latest_id orelse "");        return;    }     try appendSessionsCountUnavailableCheck(checks, alloc);} fn appendSessionDiagnosticsTruncatedCheck(    checks: *std.ArrayList(Check),    alloc: Allocator,    inspected_count: usize,) !void {    const detail = try std.fmt.allocPrint(        alloc,        "session diagnostics truncated after {d} session director{s} to keep doctor bounded",        .{ inspected_count, if (inspected_count == 1) "y" else "ies" },    );    try appendCheckOwned(checks, alloc, "session", .warn, detail);} fn appendSessionsCountUnavailableCheck(    checks: *std.ArrayList(Check),    alloc: Allocator,) !void {    try appendCheck(        checks,        alloc,        "sessions",        .warn,        "exact saved-session count unavailable without a full session scan",    );} fn appendSessionsCountCheck(    checks: *std.ArrayList(Check),    alloc: Allocator,    count: usize,    latest_id: []const u8,) !void {    if (count == 0) {        try appendCheck(checks, alloc, "sessions", .warn, "no saved sessions yet");        return;    }     const detail = try std.fmt.allocPrint(alloc, "{d} saved session(s); latest={s}", .{ count, latest_id });    try appendCheckOwned(checks, alloc, "sessions", .ok, detail);} fn appendSessionDiagnosticChecks(    checks: *std.ArrayList(Check),    alloc: Allocator,    read_store: *const session_store.Store,    diagnostics: []const session_store.DoctorDiagnostic,) !void {    for (diagnostics) |diagnostic| {        if (diagnostic.kind == .cleanup_candidate) {            const cleanup = read_store.cleanupForDoctor(                alloc,                diagnostic.session_id,            ) catch |err| {                const detail = try std.fmt.allocPrint(                    alloc,                    "session {s}: cleanup_candidate report_only=true error={s}",                    .{ diagnostic.session_id, @errorName(err) },                );                try appendCheckOwned(checks, alloc, "session", .warn, detail);                continue;            };            const detail = try std.fmt.allocPrint(                alloc,                "session {s}: cleanup_candidate cleanup_removed={d} report_only={d} ignored={d}",                .{                    diagnostic.session_id,                    cleanup.removed,                    cleanup.report_only,                    cleanup.ignored,                },            );            try appendCheckOwned(                checks,                alloc,                "session",                if (cleanup.removed > 0) .ok else .warn,                detail,            );            continue;        }         const status: CheckStatus = switch (diagnostic.kind) {            .authority_less_creation_orphan,            .authority_transition_pending,            .commit_intent_pending,            .oversized_legacy_snapshot,            .projection_missing,            .projection_stale,            .canonical_log_large,            .canonical_log_compaction_overdue,            .canonical_log_compaction_failed,            .cleanup_candidate,            => .warn,            .invalid_authority_transition,            .missing_authority,            .invalid_authority,            .invalid_commit_intent,            .projection_invalid,            .commit_watermark_missing,            .commit_watermark_invalid,            .commit_watermark_mismatched,            .canonical_state_invalid,            .unsafe_path,            => .fail,        };         var recovery_buffer: [512]u8 = undefined;        const recovery = try recoveryActionForSessionDiagnostic(            diagnostic.kind,            diagnostic.session_id,            &recovery_buffer,        );        const detail = if (diagnostic.growth_bytes) |growth_bytes|            try std.fmt.allocPrint(                alloc,                "session {s}: {s} bytes={d} growth_bytes={d} growth_frames={d}; recovery={s}",                .{                    diagnostic.session_id,                    @tagName(diagnostic.kind),                    diagnostic.bytes.?,                    growth_bytes,                    diagnostic.growth_frames.?,                    recovery,                },            )        else if (diagnostic.bytes) |bytes|            try std.fmt.allocPrint(                alloc,                "session {s}: {s} bytes={d}; recovery={s}",                .{ diagnostic.session_id, @tagName(diagnostic.kind), bytes, recovery },            )        else            try std.fmt.allocPrint(                alloc,                "session {s}: {s}{s}; recovery={s}",                .{                    diagnostic.session_id,                    @tagName(diagnostic.kind),                    if (diagnostic.kind == .authority_transition_pending)                        " report_only=true"                    else                        "",                    recovery,                },            );        try appendCheckOwned(checks, alloc, "session", status, detail);    }} fn recoveryActionForSessionDiagnostic(    kind: session_store.DoctorIssueKind,    session_id: []const u8,    buffer: []u8,) ![]const u8 {    return switch (kind) {        .authority_less_creation_orphan,        .authority_transition_pending,        .commit_intent_pending,        .cleanup_candidate,        => "rerun fx doctor after active writers exit; cleanup is guarded",         .canonical_log_large,        .canonical_log_compaction_overdue,        => "resume or update the session to trigger compaction",         .canonical_log_compaction_failed,        => "inspect the failed compaction artifact; keep it until no writer is active",         .oversized_legacy_snapshot,        => "rerun migration with --allow-large only after verifying the legacy snapshot",         .projection_missing,        .projection_stale,        => "open the session detail or resume it to rebuild projections",         .commit_watermark_missing,        .commit_watermark_invalid,        .commit_watermark_mismatched,        => std.fmt.bufPrint(            buffer,            "run fx session recover {s}; it creates a separate resumable copy and leaves the source unchanged",            .{session_id},        ),         .projection_invalid,        .canonical_state_invalid,        .invalid_commit_intent,        => std.fmt.bufPrint(            buffer,            "back up ~/.fx/sessions, then inspect this session with fx session {s} --json",            .{session_id},        ),         .missing_authority,        .invalid_authority,        .invalid_authority_transition,        .unsafe_path,        => "back up ~/.fx/sessions and avoid opening this session until the path is repaired",    };} fn appendGitCheck(checks: *std.ArrayList(Check), alloc: Allocator, workspace_root: []const u8) !void {    if (try hasGitMetadata(alloc, workspace_root)) {        try appendCheck(checks, alloc, "git", .ok, "git metadata detected for this workspace");        return;    }    try appendCheck(checks, alloc, "git", .warn, "not a git repository; pr/issue workflows will be limited");} fn appendGhCheck(checks: *std.ArrayList(Check), alloc: Allocator) !void {    if (try commandInPath(alloc, "gh")) {        try appendCheck(checks, alloc, "gh", .ok, "GitHub CLI found in PATH");        return;    }    try appendCheck(checks, alloc, "gh", .warn, "GitHub CLI not found in PATH; publish workflows unavailable");} fn resolveModel(alloc: Allocator, default_model: []const u8, configured: ?[]const u8) !ResolvedModel {    if (io_mod.getenv("FX_MODEL")) |model| {

Lines 1–500 of 892.