Class: Rubino::Tools::ReadAttachmentTool
- Inherits:
-
Base
- Object
- Base
- Rubino::Tools::ReadAttachmentTool
show all
- Defined in:
- lib/rubino/tools/read_attachment_tool.rb
Overview
Gated, on-demand attachment reader (#6). Instead of every attachment's
bytes being inlined into the prompt by default, the model calls this tool
only when it actually needs a document's content -- the single biggest
reduction in prompt-injection surface from the attachment work.
Pipeline (reuses the audited primitives; invents nothing new):
1. Attachments::Classify.call (fail-closed: lstat -> realpath-confine to
the workspace -> size cap -> magic-bytes-wins MIME). Only a safe,
policy-allowed document/text proceeds.
2. Documents.to_markdown -- in-process conversion (pdf/docx/xlsx/pptx/
html/csv/json/xml/plain). Returns nil when no in-process converter can
handle the format (e.g. the optional gem isn't installed).
3. On nil: return the existing actionable shell-extraction hint
(Preamble.document_shell_hint) -- NEVER raise, so a missing optional
gem can't break a turn.
4. Oversized Markdown is SPILLED to a persistent file and a framed
pointer is returned (read/grep it on demand) rather than dumped into
context -- the model pages it like any other large file.
5. Inline-sized Markdown is wrapped in Preamble's nonce-framed untrusted
envelope (converted document = untrusted user data).
Constant Summary
collapse
- MAX_SPILL_BYTES =
Refuse to spill a CONVERTED document larger than this (≈20MB, matching
Gemini's cap). Attachments::Classify already caps the SOURCE size; this
guards the post-conversion Markdown, which a converter can balloon.
20_000_000
Instance Attribute Summary
Attributes inherited from Base
#cancel_token, #read_tracker, #stream_chunk, #stream_kind
Instance Method Summary
collapse
Methods inherited from Base
#cancellation_requested?, #display_name, #emit_chunk, #mcp?, #risky?, #to_tool_definition, workspace_root, workspace_roots
Instance Method Details
#call(arguments) ⇒ Object
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
|
# File 'lib/rubino/tools/read_attachment_tool.rb', line 69
def call(arguments)
file_path = (arguments["file_path"] || arguments[:file_path]).to_s
return "Error: file_path is required" if file_path.empty?
cls = Attachments::Classify.call(file_path)
unless cls.safe
return "Error: cannot read #{file_path}: #{cls.reason}. " \
"Attachments must be regular files inside the workspace, under the size cap."
end
return workspace_violation_message(file_path) unless within_workspace?(cls.path)
unless Attachments::Policy.allow_kind?(cls.kind)
return "Error: #{file_path} is a #{cls.kind} (#{cls.mime}); read_attachment only " \
"reads documents and text. Inspect other kinds via the shell."
end
markdown = Rubino::Documents.to_markdown(cls.path, mime: cls.mime, cancel_token: @cancel_token)
return Attachments::Preamble.document_shell_hint(cls) if markdown.nil?
markdown = Security::Redactor.redact_sensitive_text(markdown, code_file: false)
if oversized?(markdown)
spill_oversized(cls, markdown)
else
frame(cls, markdown)
end
rescue Rubino::Interrupted
raise
rescue StandardError => e
Rubino.logger&.warn(event: "read_attachment.failed", path: file_path,
error: "#{e.class}: #{e.message}")
"Error: could not read #{file_path}: #{e.message}. " \
"Extract its text with a shell tool instead, e.g. `markitdown #{file_path}` " \
"(fallback `pdftotext #{file_path} -`, or `textutil -convert txt #{file_path}` on macOS), " \
"then read the output."
end
|
#config_key ⇒ Object
37
38
39
|
# File 'lib/rubino/tools/read_attachment_tool.rb', line 37
def config_key
"read_attachment"
end
|
#description ⇒ Object
41
42
43
44
45
46
47
48
49
50
|
# File 'lib/rubino/tools/read_attachment_tool.rb', line 41
def description
"Read an attached document on demand, converting it to Markdown IN-PROCESS " \
"(PDF, DOCX, XLSX, PPTX, HTML, CSV, JSON, XML, plain/code) and returning the " \
"text framed as untrusted user data. Prefer this over shelling out to " \
"`markitdown`/`pdftotext`. Pass the path the attachment was staged at. A " \
"document too large to inline is written to a file you then page with " \
"`read` (offset/limit) or `grep`, instead of flooding this conversation. " \
"If the format has no in-process converter, you get an actionable " \
"shell-extraction hint instead."
end
|
52
53
54
55
56
57
58
59
60
61
62
63
|
# File 'lib/rubino/tools/read_attachment_tool.rb', line 52
def input_schema
{
type: "object",
properties: {
file_path: {
type: "string",
description: "Path to the attachment to read (absolute or workspace-relative)."
}
},
required: %w[file_path]
}
end
|
#name ⇒ Object
33
34
35
|
# File 'lib/rubino/tools/read_attachment_tool.rb', line 33
def name
"read_attachment"
end
|
#risk_level ⇒ Object
65
66
67
|
# File 'lib/rubino/tools/read_attachment_tool.rb', line 65
def risk_level
:low
end
|