Class: KairosMcp::Daemon::PdfBuild

Inherits:
Object
  • Object
show all
Defined in:
lib/kairos_mcp/daemon/pdf_build.rb

Overview

PdfBuild — generate PDF from markdown via pandoc + xelatex in sandbox.

Design (P3.3, Phase 3 v0.2 §4):

Uses RestrictedShell to invoke pandoc with network: :deny.
All work happens in a temp build directory; output is moved to
the target path atomically.

Constant Summary collapse

DEFAULT_TIMEOUT =

seconds

120
DEFAULT_ENGINE =
'xelatex'

Class Method Summary collapse

Class Method Details

.available?Boolean

Check if pandoc is available.

Returns:

  • (Boolean)


88
89
90
91
92
93
# File 'lib/kairos_mcp/daemon/pdf_build.rb', line 88

def self.available?
  RestrictedShell::BinaryResolver.resolve!('pandoc')
  true
rescue RestrictedShell::ResolverError
  false
end

.build(markdown_path:, output_path:, workspace_root:, template: nil, timeout: DEFAULT_TIMEOUT) ⇒ Hash

Returns { status:, input_hash:, output_hash:, duration_ms: }.

Parameters:

  • markdown_path (String)

    absolute path to input markdown

  • output_path (String)

    absolute path for output PDF

  • workspace_root (String)

    workspace root for confinement

  • template (String, nil) (defaults to: nil)

    pandoc template name (optional)

  • timeout (Integer) (defaults to: DEFAULT_TIMEOUT)

    seconds (default 120)

Returns:

  • (Hash)

    { status:, input_hash:, output_hash:, duration_ms: }

Raises:

  • (ArgumentError)


26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/kairos_mcp/daemon/pdf_build.rb', line 26

def self.build(markdown_path:, output_path:, workspace_root:,
               template: nil, timeout: DEFAULT_TIMEOUT)
  raise ArgumentError, 'markdown_path must be absolute' unless markdown_path.start_with?('/')
  raise ArgumentError, 'output_path must be absolute' unless output_path.start_with?('/')
  raise ArgumentError, 'markdown file not found' unless File.file?(markdown_path)

  input_hash = "sha256:#{Digest::SHA256.file(markdown_path).hexdigest}"

  Dir.mktmpdir('kairos_pdf_build') do |build_dir|
    out_file = File.join(build_dir, 'output.pdf')

    # Build pandoc command
    cmd = ['pandoc', markdown_path, '-o', out_file,
           "--pdf-engine=#{DEFAULT_ENGINE}",
           '--pdf-engine-opt=-no-shell-escape']
    cmd += ['--template', template] if template

    result = RestrictedShell.run(
      cmd: cmd,
      cwd: build_dir,
      timeout: timeout,
      allowed_paths: [File.dirname(markdown_path), build_dir, workspace_root],
      network: :deny
    )

    unless result.success?
      return {
        status: 'failed',
        exit_code: result.status,
        stderr: result.stderr[0, 2000],
        input_hash: input_hash,
        duration_ms: result.duration_ms
      }
    end

    unless File.file?(out_file)
      return {
        status: 'failed',
        error: 'pandoc produced no output file',
        input_hash: input_hash,
        duration_ms: result.duration_ms
      }
    end

    # Atomic move to target
    FileUtils.mkdir_p(File.dirname(output_path))
    FileUtils.mv(out_file, output_path)

    output_hash = "sha256:#{Digest::SHA256.file(output_path).hexdigest}"

    {
      status: 'ok',
      input_hash: input_hash,
      output_hash: output_hash,
      output_path: output_path,
      duration_ms: result.duration_ms,
      sandbox_driver: result.sandbox_driver
    }
  end
end