Class: KairosMcp::Daemon::PdfBuild
- Inherits:
-
Object
- Object
- KairosMcp::Daemon::PdfBuild
- 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
-
.available? ⇒ Boolean
Check if pandoc is available.
-
.build(markdown_path:, output_path:, workspace_root:, template: nil, timeout: DEFAULT_TIMEOUT) ⇒ Hash
{ status:, input_hash:, output_hash:, duration_ms: }.
Class Method Details
.available? ⇒ Boolean
Check if pandoc is available.
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: }.
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 |