robot_lab 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/README.md +1 -1
  4. data/lib/robot_lab/mcp/transports/streamable_http.rb +1 -1
  5. data/lib/robot_lab/version.rb +1 -1
  6. metadata +3 -253
  7. data/.architecture/AGENTS.md +0 -32
  8. data/.architecture/config.yml +0 -8
  9. data/.architecture/members.yml +0 -60
  10. data/.architecture/reviews/feature-free-will.md +0 -490
  11. data/.architecture/reviews/overall-codebase.md +0 -427
  12. data/.claude/settings.local.json +0 -57
  13. data/.codex/config.toml +0 -2
  14. data/.envrc +0 -6
  15. data/.github/workflows/deploy-github-pages.yml +0 -52
  16. data/.irbrc +0 -6
  17. data/.loki +0 -65
  18. data/.quality/reek_baseline.txt +0 -43
  19. data/.rubocop.yml +0 -10
  20. data/Archspec.rb +0 -44
  21. data/CLAUDE.md +0 -141
  22. data/COMMITS.md +0 -196
  23. data/Rakefile +0 -193
  24. data/_typos.toml +0 -21
  25. data/agent2agent_review.md +0 -192
  26. data/agentf_improvements.md +0 -253
  27. data/agents.md +0 -14
  28. data/docs/api/core/index.md +0 -147
  29. data/docs/api/core/memory.md +0 -578
  30. data/docs/api/core/network.md +0 -553
  31. data/docs/api/core/result.md +0 -211
  32. data/docs/api/core/robot.md +0 -1667
  33. data/docs/api/core/state.md +0 -145
  34. data/docs/api/core/tool.md +0 -520
  35. data/docs/api/errors.md +0 -323
  36. data/docs/api/hooks.md +0 -469
  37. data/docs/api/index.md +0 -137
  38. data/docs/api/mcp/client.md +0 -387
  39. data/docs/api/mcp/index.md +0 -267
  40. data/docs/api/mcp/server.md +0 -252
  41. data/docs/api/mcp/transports.md +0 -339
  42. data/docs/api/messages/index.md +0 -126
  43. data/docs/api/messages/text-message.md +0 -170
  44. data/docs/api/messages/tool-call-message.md +0 -206
  45. data/docs/api/messages/tool-result-message.md +0 -242
  46. data/docs/api/messages/user-message.md +0 -232
  47. data/docs/api/skills.md +0 -423
  48. data/docs/api/streaming/context.md +0 -280
  49. data/docs/api/streaming/events.md +0 -185
  50. data/docs/api/streaming/index.md +0 -156
  51. data/docs/api/support.md +0 -846
  52. data/docs/architecture/core-concepts.md +0 -575
  53. data/docs/architecture/index.md +0 -252
  54. data/docs/architecture/message-flow.md +0 -366
  55. data/docs/architecture/network-orchestration.md +0 -478
  56. data/docs/architecture/robot-execution.md +0 -396
  57. data/docs/architecture/state-management.md +0 -410
  58. data/docs/assets/css/custom.css +0 -56
  59. data/docs/assets/images/robot_lab.jpg +0 -0
  60. data/docs/concepts.md +0 -641
  61. data/docs/examples/basic-chat.md +0 -258
  62. data/docs/examples/index.md +0 -425
  63. data/docs/examples/mcp-server.md +0 -403
  64. data/docs/examples/multi-robot-network.md +0 -476
  65. data/docs/examples/tool-usage.md +0 -468
  66. data/docs/getting-started/configuration.md +0 -645
  67. data/docs/getting-started/index.md +0 -56
  68. data/docs/getting-started/installation.md +0 -197
  69. data/docs/getting-started/quick-start.md +0 -293
  70. data/docs/guides/building-robots.md +0 -1187
  71. data/docs/guides/creating-networks.md +0 -736
  72. data/docs/guides/hooks.md +0 -1206
  73. data/docs/guides/index.md +0 -78
  74. data/docs/guides/knowledge.md +0 -219
  75. data/docs/guides/mcp-integration.md +0 -740
  76. data/docs/guides/memory.md +0 -458
  77. data/docs/guides/observability.md +0 -733
  78. data/docs/guides/streaming.md +0 -323
  79. data/docs/guides/using-tools.md +0 -780
  80. data/docs/index.md +0 -280
  81. data/docs/superpowers/plans/2026-05-06-agentskills.md +0 -1303
  82. data/docs/superpowers/specs/2026-05-06-agentskills-design.md +0 -247
  83. data/examples/.envrc +0 -3
  84. data/examples/01_simple_robot.rb +0 -31
  85. data/examples/02_tools.rb +0 -109
  86. data/examples/03_network.rb +0 -118
  87. data/examples/04_mcp.rb +0 -217
  88. data/examples/05_streaming.rb +0 -137
  89. data/examples/06_prompt_templates.rb +0 -325
  90. data/examples/07_network_memory.rb +0 -313
  91. data/examples/08_llm_config.rb +0 -162
  92. data/examples/09_chaining.rb +0 -251
  93. data/examples/10_memory.rb +0 -315
  94. data/examples/11_network_introspection.rb +0 -239
  95. data/examples/12_message_bus.rb +0 -67
  96. data/examples/13_spawn.rb +0 -85
  97. data/examples/14_rusty_circuit/.envrc +0 -1
  98. data/examples/14_rusty_circuit/.gitignore +0 -1
  99. data/examples/14_rusty_circuit/README.md +0 -1
  100. data/examples/14_rusty_circuit/comic.rb +0 -149
  101. data/examples/14_rusty_circuit/display.rb +0 -217
  102. data/examples/14_rusty_circuit/heckler.rb +0 -65
  103. data/examples/14_rusty_circuit/open_mic.rb +0 -132
  104. data/examples/14_rusty_circuit/prompts/open_mic_comic.md +0 -20
  105. data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +0 -23
  106. data/examples/14_rusty_circuit/prompts/open_mic_scout.md +0 -20
  107. data/examples/14_rusty_circuit/scout.rb +0 -165
  108. data/examples/14_rusty_circuit/show.log +0 -234
  109. data/examples/15_memory_network_and_bus/.envrc +0 -1
  110. data/examples/15_memory_network_and_bus/README.md +0 -66
  111. data/examples/15_memory_network_and_bus/editor_in_chief.rb +0 -24
  112. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +0 -213
  113. data/examples/15_memory_network_and_bus/linux_writer.rb +0 -80
  114. data/examples/15_memory_network_and_bus/os_editor.rb +0 -48
  115. data/examples/15_memory_network_and_bus/os_writer.rb +0 -49
  116. data/examples/15_memory_network_and_bus/output/combined_article.md +0 -11
  117. data/examples/15_memory_network_and_bus/output/final_article.md +0 -8
  118. data/examples/15_memory_network_and_bus/output/linux_draft.md +0 -7
  119. data/examples/15_memory_network_and_bus/output/mac_draft.md +0 -7
  120. data/examples/15_memory_network_and_bus/output/memory.json +0 -13
  121. data/examples/15_memory_network_and_bus/output/revision_1.md +0 -29
  122. data/examples/15_memory_network_and_bus/output/revision_2.md +0 -50
  123. data/examples/15_memory_network_and_bus/output/revision_3.md +0 -8
  124. data/examples/15_memory_network_and_bus/output/windows_draft.md +0 -7
  125. data/examples/15_memory_network_and_bus/prompts/os_advocate.md +0 -13
  126. data/examples/15_memory_network_and_bus/prompts/os_chief.md +0 -13
  127. data/examples/15_memory_network_and_bus/prompts/os_editor.md +0 -13
  128. data/examples/16_writers_room/.envrc +0 -1
  129. data/examples/16_writers_room/display.rb +0 -158
  130. data/examples/16_writers_room/output/.gitignore +0 -4
  131. data/examples/16_writers_room/output/README.md +0 -69
  132. data/examples/16_writers_room/output/opus_001.md +0 -263
  133. data/examples/16_writers_room/output/opus_001_notes.log +0 -470
  134. data/examples/16_writers_room/output/opus_002.md +0 -245
  135. data/examples/16_writers_room/output/opus_002_notes.log +0 -546
  136. data/examples/16_writers_room/output/opus_002_screenplay.md +0 -7989
  137. data/examples/16_writers_room/output/opus_002_screenplay_notes.md +0 -993
  138. data/examples/16_writers_room/prompts/screenplay_writer.md +0 -66
  139. data/examples/16_writers_room/prompts/writer.md +0 -37
  140. data/examples/16_writers_room/room.rb +0 -186
  141. data/examples/16_writers_room/tools.rb +0 -173
  142. data/examples/16_writers_room/writer.rb +0 -121
  143. data/examples/16_writers_room/writers_room.rb +0 -256
  144. data/examples/17_skills.rb +0 -234
  145. data/examples/18_rails/.envrc +0 -3
  146. data/examples/18_rails/.gitignore +0 -5
  147. data/examples/18_rails/Gemfile +0 -11
  148. data/examples/18_rails/README.md +0 -67
  149. data/examples/18_rails/Rakefile +0 -4
  150. data/examples/18_rails/app/controllers/application_controller.rb +0 -4
  151. data/examples/18_rails/app/controllers/chat_controller.rb +0 -50
  152. data/examples/18_rails/app/jobs/application_job.rb +0 -4
  153. data/examples/18_rails/app/jobs/robot_run_job.rb +0 -25
  154. data/examples/18_rails/app/models/application_record.rb +0 -5
  155. data/examples/18_rails/app/models/robot_lab_result.rb +0 -36
  156. data/examples/18_rails/app/models/robot_lab_thread.rb +0 -23
  157. data/examples/18_rails/app/robots/chat_robot.rb +0 -25
  158. data/examples/18_rails/app/tools/time_tool.rb +0 -9
  159. data/examples/18_rails/app/views/chat/_user_message.html.erb +0 -1
  160. data/examples/18_rails/app/views/chat/index.html.erb +0 -67
  161. data/examples/18_rails/app/views/layouts/application.html.erb +0 -49
  162. data/examples/18_rails/bin/dev +0 -7
  163. data/examples/18_rails/bin/rails +0 -6
  164. data/examples/18_rails/bin/setup +0 -15
  165. data/examples/18_rails/config/application.rb +0 -33
  166. data/examples/18_rails/config/cable.yml +0 -2
  167. data/examples/18_rails/config/database.yml +0 -5
  168. data/examples/18_rails/config/environment.rb +0 -4
  169. data/examples/18_rails/config/initializers/robot_lab.rb +0 -11
  170. data/examples/18_rails/config/routes.rb +0 -6
  171. data/examples/18_rails/config.ru +0 -4
  172. data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +0 -32
  173. data/examples/19_token_tracking.rb +0 -138
  174. data/examples/20_circuit_breaker.rb +0 -147
  175. data/examples/21_learning_loop.rb +0 -181
  176. data/examples/22_context_compression.rb +0 -172
  177. data/examples/23_convergence.rb +0 -133
  178. data/examples/24_structured_delegation.rb +0 -151
  179. data/examples/25_history_search/conversation.jsonl +0 -30
  180. data/examples/25_history_search.rb +0 -133
  181. data/examples/26_document_store/api_versioning_adr.md +0 -52
  182. data/examples/26_document_store/incident_postmortem.md +0 -46
  183. data/examples/26_document_store/postgres_runbook.md +0 -49
  184. data/examples/26_document_store/redis_caching_guide.md +0 -48
  185. data/examples/26_document_store/sidekiq_guide.md +0 -51
  186. data/examples/26_document_store.rb +0 -140
  187. data/examples/27_incident_response/README.md +0 -65
  188. data/examples/27_incident_response/incident_response.rb +0 -261
  189. data/examples/28_mcp_discovery.rb +0 -113
  190. data/examples/29_ractor_tools.rb +0 -240
  191. data/examples/30_ractor_network.rb +0 -252
  192. data/examples/31_launch_assessment.rb +0 -246
  193. data/examples/32_newsletter_reader.rb +0 -193
  194. data/examples/33_stock_generator.rb +0 -80
  195. data/examples/33_stock_predictor.rb +0 -327
  196. data/examples/34_agentskills.rb +0 -76
  197. data/examples/35_hooks.rb +0 -266
  198. data/examples/README.md +0 -550
  199. data/examples/common.rb +0 -180
  200. data/examples/prompts/assistant.md +0 -5
  201. data/examples/prompts/audit_trail.md +0 -8
  202. data/examples/prompts/billing.md +0 -10
  203. data/examples/prompts/classifier.md +0 -7
  204. data/examples/prompts/comedian.md +0 -6
  205. data/examples/prompts/comedy_critic.md +0 -10
  206. data/examples/prompts/configurable.md +0 -9
  207. data/examples/prompts/dispatcher.md +0 -12
  208. data/examples/prompts/entity_extractor.md +0 -14
  209. data/examples/prompts/escalation.md +0 -42
  210. data/examples/prompts/frontmatter_mcp_test.md +0 -9
  211. data/examples/prompts/frontmatter_named_test.md +0 -5
  212. data/examples/prompts/frontmatter_tools_test.md +0 -6
  213. data/examples/prompts/general.md +0 -7
  214. data/examples/prompts/github_assistant.md +0 -14
  215. data/examples/prompts/helper.md +0 -4
  216. data/examples/prompts/incident_responder.md +0 -18
  217. data/examples/prompts/keyword_extractor.md +0 -11
  218. data/examples/prompts/llm_config_demo.md +0 -20
  219. data/examples/prompts/order_support.md +0 -35
  220. data/examples/prompts/os_advocate.md +0 -13
  221. data/examples/prompts/os_chief.md +0 -13
  222. data/examples/prompts/os_editor.md +0 -13
  223. data/examples/prompts/parameterized_main_test.md +0 -6
  224. data/examples/prompts/pii_redactor.md +0 -7
  225. data/examples/prompts/product_support.md +0 -37
  226. data/examples/prompts/runbook_protocol.md +0 -12
  227. data/examples/prompts/sentiment_analyzer.md +0 -12
  228. data/examples/prompts/skill_a_test.md +0 -4
  229. data/examples/prompts/skill_b_test.md +0 -4
  230. data/examples/prompts/skill_config_test.md +0 -5
  231. data/examples/prompts/skill_cycle_a_test.md +0 -6
  232. data/examples/prompts/skill_cycle_b_test.md +0 -6
  233. data/examples/prompts/skill_description_test.md +0 -4
  234. data/examples/prompts/skill_leaf_test.md +0 -4
  235. data/examples/prompts/skill_nested_test.md +0 -6
  236. data/examples/prompts/skill_refs_main_test.md +0 -6
  237. data/examples/prompts/skill_self_ref_test.md +0 -6
  238. data/examples/prompts/skill_with_mcp_test.md +0 -9
  239. data/examples/prompts/skill_with_params_test.md +0 -6
  240. data/examples/prompts/skill_with_robot_name_test.md +0 -5
  241. data/examples/prompts/skill_with_tools_test.md +0 -6
  242. data/examples/prompts/sre_compliance.md +0 -10
  243. data/examples/prompts/structured_output.md +0 -7
  244. data/examples/prompts/synthesizer.md +0 -17
  245. data/examples/prompts/technical.md +0 -10
  246. data/examples/prompts/template_with_skills_test.md +0 -6
  247. data/examples/prompts/triage.md +0 -22
  248. data/examples/ruboruby.md +0 -423
  249. data/examples/run_all.rb +0 -60
  250. data/examples/xyzzy.rb +0 -97
  251. data/logfile +0 -8
  252. data/mkdocs.yml +0 -209
  253. data/robot_concurrency.md +0 -38
  254. data/simple_acp_review.md +0 -298
  255. data/temp.md +0 -6
  256. data/tool_manifest_plan.md +0 -155
data/docs/api/skills.md DELETED
@@ -1,423 +0,0 @@
1
- # Skills API
2
-
3
- Class-level reference for the AgentSkills subsystem: skill bundles, the scripts
4
- they expose as tools, and the capabilities those scripts declare. Core itself
5
- has **no sandboxing and no execution limitations** — every script runs
6
- unconfined until the optional
7
- [`robot_lab-sandbox`](https://github.com/MadBomber/robot_lab-sandbox) gem is
8
- required, at which point it installs a confinement strategy (see
9
- [ScriptTool.executor](#scripttoolexecutor) below). For the how-to, see
10
- [Using Tools: Skill Scripts and Sandboxing](../guides/using-tools.md#skill-scripts-and-sandboxing)
11
- and [Building Robots: Composable Skills](../guides/building-robots.md#composable-skills).
12
-
13
- !!! note "Two different things are called 'skills'"
14
- **Template skills** (`RobotLab.build(skills: [:clarifier])`) are ordinary
15
- prompt templates whose bodies are prepended to a robot's system prompt — see
16
- [Robot: Skills](core/robot.md#skills). **AgentSkills** (this page) are
17
- `SKILL.md` *bundles* on disk, discovered from `~/.prompts/skills/`, matched
18
- to a message by embedding similarity at run time, and capable of contributing
19
- executable tools. They share the word but not the mechanism.
20
-
21
- ```mermaid
22
- graph LR
23
- subgraph "Discovery"
24
- C[AgentSkillCatalog<br/>~/.prompts/skills/]
25
- S[AgentSkill<br/>SKILL.md]
26
- end
27
-
28
- subgraph "Capability grant"
29
- D[Capabilities<br/>declared in front matter]
30
- CE[Capabilities.ceiling<br/>from config.sandbox]
31
- G[effective grant<br/>declared ∩ ceiling]
32
- end
33
-
34
- subgraph "Execution (core)"
35
- T[ScriptTool.from_path<br/>-> RobotLab::Tool]
36
- EX{ScriptTool.executor<br/>set?}
37
- UN[Open3.capture2e<br/>unconfined, no timeout]
38
- end
39
-
40
- subgraph "robot_lab-sandbox (optional gem)"
41
- SB{Sandbox.enabled?}
42
- SE[Sandbox::Seatbelt<br/>macOS]
43
- NU[Sandbox::Null<br/>passthrough]
44
- end
45
-
46
- R[Robot<br/>AgentSkillMatching] --> C
47
- C --> S
48
- S --> D
49
- S --> T
50
- D --> G
51
- CE --> G
52
- T --> EX
53
- EX -- "nil (default)" --> UN
54
- EX -- "installed" --> SB
55
- SB -- "off, or trust: core,<br/>or non-macOS" --> NU
56
- SB -- "on + macOS" --> SE
57
- G --> SE
58
- ```
59
-
60
- ---
61
-
62
- ## RobotLab::AgentSkill
63
-
64
- Immutable value object for one skill folder: a directory containing a `SKILL.md`
65
- with `name` and `description` front matter, plus optional `scripts/`,
66
- `references/`, and `assets/` subdirectories.
67
-
68
- ### Constructor
69
-
70
- ```ruby
71
- skill = RobotLab::AgentSkill.new("~/.prompts/skills/deploy-checker/SKILL.md")
72
- ```
73
-
74
- | Name | Type | Description |
75
- |------|------|-------------|
76
- | `skill_md_path` | `String`, `Pathname` | Path to the `SKILL.md` file itself, **not** the directory |
77
-
78
- **Raises `RobotLab::ConfigurationError`** when front matter is missing `name` or
79
- `description` (or either is blank) — and because a file with no `---` block
80
- parses to an empty hash, a `SKILL.md` without front matter always raises.
81
- Malformed YAML raises `Psych::SyntaxError` instead, straight from
82
- `YAML.safe_load`. [`AgentSkillCatalog`](#robotlabagentskillcatalog) rescues both
83
- and skips the bundle; construct an `AgentSkill` directly and you get the
84
- exception.
85
-
86
- ### Attributes
87
-
88
- | Attribute | Type | Description |
89
- |-----------|------|-------------|
90
- | `name` | `String` | Front-matter `name`; also the catalog lookup key (symbolized) |
91
- | `description` | `String` | Front-matter `description`; the text matched against the user's message |
92
- | `path` | `Pathname` | The skill **directory** (`dirname` of the `SKILL.md` path) |
93
- | `capabilities` | `Capabilities` | Built from front matter via `Capabilities.from_front_matter` |
94
-
95
- ### instructions
96
-
97
- ```ruby
98
- skill.instructions # => String
99
- ```
100
-
101
- The `SKILL.md` body below the front matter, stripped. This is the text
102
- `Robot::AgentSkillMatching` prepends to the system prompt when the skill matches.
103
- Memoized.
104
-
105
- ### scripts
106
-
107
- ```ruby
108
- skill.scripts # => Array<Pathname>
109
- ```
110
-
111
- Every **file** directly inside the skill's `scripts/` directory, sorted. Returns
112
- `[]` when there is no `scripts/` directory. Not recursive — subdirectories are
113
- skipped. Memoized.
114
-
115
- ### script_tools
116
-
117
- ```ruby
118
- skill.script_tools # => Array<RobotLab::Tool>
119
- ```
120
-
121
- One `RobotLab::Tool` per script, built with
122
- [`ScriptTool.from_path`](#scripttoolfrom_path) and carrying this skill's
123
- `capabilities` and directory. **Non-executable scripts are skipped** (logged at
124
- `warn` and filtered out by `filter_map`), so this array can be shorter than
125
- `scripts`. Memoized.
126
-
127
- These tools are appended to `robot.local_tools` for the duration of a matched
128
- `run` and removed again in the `ensure` block — see
129
- [Robot Execution](../architecture/robot-execution.md#execution-overview).
130
-
131
- ---
132
-
133
- ## RobotLab::AgentSkillCatalog
134
-
135
- Lazily-loaded registry of the skill folders under a root directory.
136
-
137
- ### SKILLS_ROOT
138
-
139
- ```ruby
140
- RobotLab::AgentSkillCatalog::SKILLS_ROOT
141
- # => #<Pathname:/Users/you/.prompts/skills>
142
- ```
143
-
144
- `~/.prompts/skills`, expanded at load time. The path the process-level singleton
145
- scans.
146
-
147
- ### instance / reset!
148
-
149
- ```ruby
150
- RobotLab::AgentSkillCatalog.instance # => the singleton, scanning SKILLS_ROOT
151
- RobotLab::AgentSkillCatalog.reset! # => nil; next `instance` builds a fresh one
152
- ```
153
-
154
- `instance` memoizes. `reset!` drops the memo — it exists so tests can point the
155
- catalog at a fixture directory by resetting and constructing an instance
156
- explicitly with a different root.
157
-
158
- ### Constructor
159
-
160
- ```ruby
161
- catalog = RobotLab::AgentSkillCatalog.new("/path/to/skills")
162
- ```
163
-
164
- | Name | Type | Default | Description |
165
- |------|------|---------|-------------|
166
- | `skills_root` | `String`, `Pathname` | `SKILLS_ROOT` | Directory to scan |
167
-
168
- Construction does **no** I/O; the scan happens on the first `find`/`all`.
169
-
170
- ### find
171
-
172
- ```ruby
173
- catalog.find(:deploy_checker) # => AgentSkill or nil
174
- catalog.find("deploy-checker") # => AgentSkill or nil
175
- ```
176
-
177
- Look up by skill **name** (the front-matter `name`, symbolized) — not by
178
- directory name, and not by file path. Returns `nil` when not found.
179
-
180
- ### all
181
-
182
- ```ruby
183
- catalog.all # => Array<AgentSkill>
184
- ```
185
-
186
- Every successfully-loaded skill.
187
-
188
- !!! note "Loading is lazy, thread-safe, and forgiving"
189
- The scan runs once, under a `Mutex`, on the first `find` or `all`. A missing
190
- root directory is not an error — the catalog is simply empty. A directory
191
- without a `SKILL.md` is skipped silently; a `SKILL.md` that raises
192
- `ConfigurationError` or `Psych::SyntaxError` is skipped with a `warn`
193
- (`"AgentSkillCatalog: <message>, skipping <dir>"`). One bad bundle never
194
- prevents the others from loading, and the scan is never retried.
195
-
196
- ---
197
-
198
- ## RobotLab::Capabilities
199
-
200
- What a skill's scripts may read, write, reach, and how long they may run.
201
-
202
- A skill declares what it **wants** in `SKILL.md` front matter; the global
203
- `sandbox:` config declares the **ceiling**. The effective grant is the
204
- [intersection](#intersect) of the two.
205
-
206
- ### Constants
207
-
208
- | Constant | Value | Description |
209
- |----------|-------|-------------|
210
- | `DEFAULT_TIMEOUT` | `60` | Seconds, used when `timeout` is absent or non-positive |
211
- | `TRUST_LEVELS` | `["core", "external"]` | Any other value falls back to `"external"` |
212
-
213
- ### Constructor
214
-
215
- ```ruby
216
- RobotLab::Capabilities.new(
217
- fs_read: [], fs_write: [], network: false,
218
- timeout: DEFAULT_TIMEOUT, trust: "external"
219
- )
220
- ```
221
-
222
- | Name | Type | Default | Coercion |
223
- |------|------|---------|----------|
224
- | `fs_read` | `Array<String>` | `[]` | `Array(...)` then `to_s` on each entry |
225
- | `fs_write` | `Array<String>` | `[]` | Same |
226
- | `network` | `Boolean` | `false` | Any truthy value becomes `true` |
227
- | `timeout` | `Integer` | `60` | `to_i`; anything not positive becomes `DEFAULT_TIMEOUT` |
228
- | `trust` | `String` | `"external"` | Must be in `TRUST_LEVELS`, else `"external"` |
229
-
230
- Every value is normalized in the constructor, so the readers `fs_read`,
231
- `fs_write`, `network`, `timeout`, and `trust` always return well-formed values —
232
- a malformed `SKILL.md` degrades to the safe default rather than raising.
233
-
234
- ### from_front_matter
235
-
236
- ```ruby
237
- RobotLab::Capabilities.from_front_matter(front_matter_hash) # => Capabilities
238
- ```
239
-
240
- Build from a parsed `SKILL.md` front-matter hash, reading `fs_read`, `fs_write`,
241
- `network`, `timeout`, and `trust`. A `nil` front matter yields an all-defaults
242
- instance.
243
-
244
- ### fm_value
245
-
246
- ```ruby
247
- RobotLab::Capabilities.fm_value(front_matter, :network, false)
248
- ```
249
-
250
- Look up a front-matter key tolerating **either** string or symbol keys (string
251
- first, then symbol), returning `default` when both are `nil`. Exposed because
252
- `from_front_matter` uses it and skill-tooling may need the same leniency.
253
-
254
- ### ceiling
255
-
256
- ```ruby
257
- RobotLab::Capabilities.ceiling # => from RobotLab.config.sandbox
258
- RobotLab::Capabilities.ceiling(custom_config)
259
- ```
260
-
261
- The maximum grant any skill may receive, read from the config's `sandbox:`
262
- section. When there is no `sandbox` section at all, the ceiling is
263
- `Capabilities.new(fs_read: ["."])` — read-only access to the working directory,
264
- no writes, no network.
265
-
266
- Note the ceiling never carries a `trust` — trust is a property of the skill, not
267
- of the ceiling, and `intersect` keeps the declared value.
268
-
269
- ### core?
270
-
271
- ```ruby
272
- capabilities.core? # => trust == "core"
273
- ```
274
-
275
- A `core` skill is exempt from confinement: when `robot_lab-sandbox` is loaded,
276
- `Sandbox.for` returns a `Null` strategy for it regardless of platform or
277
- config. Reserve `trust: core` for bundles you wrote and audited.
278
-
279
- ### intersect
280
-
281
- ```ruby
282
- grant = declared.intersect(RobotLab::Capabilities.ceiling)
283
- ```
284
-
285
- The effective grant. Per field:
286
-
287
- | Field | Rule |
288
- |-------|------|
289
- | `fs_read` / `fs_write` | A requested path survives only when it **is** a ceiling root or lives beneath one. Both sides are `File.expand_path`ed before comparison, so `~` and relative paths resolve first |
290
- | `network` | `declared && ceiling` — both must allow it |
291
- | `timeout` | The **smaller** of the two |
292
- | `trust` | The **declared** value, unchanged |
293
-
294
- Because the check is prefix-based on expanded paths, a ceiling of `["."]` grants
295
- nothing outside the working directory even if a skill asks for `/etc`.
296
-
297
- ---
298
-
299
- ## RobotLab::ScriptTool
300
-
301
- Factory module that turns an executable script into a `RobotLab::Tool`. All
302
- methods are module functions.
303
-
304
- ### ScriptTool.from_path
305
-
306
- ```ruby
307
- tool = RobotLab::ScriptTool.from_path(script_path, capabilities: nil, skill_dir: nil)
308
- # => RobotLab::Tool, or nil
309
- ```
310
-
311
- | Name | Type | Default | Description |
312
- |------|------|---------|-------------|
313
- | `script_path` | `String`, `Pathname` | **required** | The script file |
314
- | `capabilities` | `Capabilities`, `nil` | `nil` → `Capabilities.new` | The skill's declared capabilities |
315
- | `skill_dir` | `String`, `nil` | `nil` → the script's own directory | Bundle root; always granted read access under Seatbelt |
316
-
317
- **Returns `nil`** when the file is not executable, logging
318
- `"ScriptTool: <basename> is not executable, skipping"` at `warn`. It never raises.
319
-
320
- The generated tool takes a single optional `args` string parameter, which is
321
- `Shellwords.split` and appended to `bash <script>`. Its name comes from
322
- [`derive_name`](#scripttoolderive_name) and its description from
323
- [`extract_description`](#scripttoolextract_description).
324
-
325
- ### ScriptTool.executor
326
-
327
- ```ruby
328
- RobotLab::ScriptTool.executor # => #call, or nil (the default)
329
- RobotLab::ScriptTool.executor = obj # any object responding to
330
- # call(cmd, capabilities:, skill_dir:)
331
- ```
332
-
333
- The extension point core exposes for confinement. `nil` by default — core has
334
- no sandboxing of its own. `robot_lab-sandbox`, when required, sets this to
335
- `RobotLab::Sandbox::Executor`, which handles `capabilities`/timeout/cleanup
336
- itself. See that gem's docs for what it does when installed.
337
-
338
- ### ScriptTool.execute
339
-
340
- ```ruby
341
- RobotLab::ScriptTool.execute(cmd, capabilities:, skill_dir:) # => String
342
- ```
343
-
344
- Run a command array and return its combined stdout+stderr, or an error string.
345
- Two paths:
346
-
347
- - **`executor` is `nil`** (the default) — `Open3.capture2e`, unconfined, **no timeout**.
348
- - **`executor` is set** — delegates entirely to `executor.call(cmd, capabilities:, skill_dir:)`.
349
- Core no longer knows or cares what the executor does with `capabilities` or
350
- how (or whether) it bounds execution time.
351
-
352
- ### ScriptTool.format_result
353
-
354
- ```ruby
355
- RobotLab::ScriptTool.format_result(output, status) # => String
356
- ```
357
-
358
- | `status` | Result |
359
- |----------|--------|
360
- | `nil` | `"Error (timed out):\n<output>"` |
361
- | success | `output` verbatim |
362
- | non-zero exit | `"Error (exit <N>):\n<output>"` |
363
-
364
- Failures come back as **text for the LLM**, not exceptions — the model sees the
365
- error and can adapt.
366
-
367
- ### ScriptTool.derive_name
368
-
369
- ```ruby
370
- RobotLab::ScriptTool.derive_name(Pathname.new("check-deploy.sh")) # => "check_deploy"
371
- ```
372
-
373
- Strips the final extension, replaces every run of non-alphanumerics with `_`, and
374
- trims leading/trailing underscores.
375
-
376
- ### ScriptTool.extract_description
377
-
378
- ```ruby
379
- RobotLab::ScriptTool.extract_description(path) # => String
380
- ```
381
-
382
- The first non-shebang comment line in the file, with leading `#` and whitespace
383
- removed. Falls back to `derive_name(path)` when there is no comment or the file
384
- cannot be read.
385
-
386
- ```bash
387
- #!/usr/bin/env bash
388
- # Verifies a deployment's health before promoting it. <- becomes the description
389
- ```
390
-
391
- ---
392
-
393
- ## Confinement: robot_lab-sandbox
394
-
395
- `RobotLab::Sandbox`, `RobotLab::Sandbox::Seatbelt`, and `RobotLab::Sandbox::Null`
396
- used to live here; they now ship in the separate
397
- [`robot_lab-sandbox`](https://github.com/MadBomber/robot_lab-sandbox) gem, which
398
- core has no dependency on. Requiring it installs `RobotLab::Sandbox::Executor`
399
- as [`ScriptTool.executor`](#scripttoolexecutor):
400
-
401
- - `Sandbox.enabled?` — reads `config.sandbox.enabled` (default `false`).
402
- - `Sandbox.for(grant, skill_dir:, macos: macos?)` — picks `Sandbox::Seatbelt` on
403
- macOS, `Sandbox::Null` (passthrough) elsewhere or for `trust: core` grants.
404
- - `Sandbox::Seatbelt` — generates a deny-by-default `sandbox-exec` profile from
405
- the effective grant (`fs_read`/`fs_write`/`network`), wrapping the command as
406
- `sandbox-exec -f <profile> <cmd...>`. `$HOME` is never implicitly readable.
407
- - `Sandbox::Null` — `wrap(cmd)` returns `cmd` unchanged; `cleanup` is a no-op.
408
- - `Sandbox::Executor` — the piece that plugs into core: intersects the skill's
409
- `Capabilities` with `Capabilities.ceiling`, wraps and runs the command under
410
- the chosen strategy, and bounds it with the grant's `timeout`
411
- (`Process.kill('-TERM', ...)` on the process group on expiry).
412
-
413
- Full reference lives in that gem's own docs.
414
-
415
- ---
416
-
417
- ## See Also
418
-
419
- - [Using Tools: Skill Scripts and Sandboxing](../guides/using-tools.md#skill-scripts-and-sandboxing)
420
- - [Configuration: `sandbox:` section](../getting-started/configuration.md#skill-script-sandboxing-sandbox-section)
421
- - [robot_lab-sandbox](https://github.com/MadBomber/robot_lab-sandbox) — the optional confinement gem
422
- - [Robot: Skills](core/robot.md#skills) — template skills, the other meaning
423
- - [Tool](core/tool.md) — `Tool.create`, which `ScriptTool` builds on
@@ -1,280 +0,0 @@
1
- # Streaming::Context
2
-
3
- Manages streaming event publishing with automatic sequencing, timestamping, and ID generation.
4
-
5
- > **Not wired into the framework.** No robot, network, task, or hook ever
6
- > constructs a `Streaming::Context` — `grep -rn "Streaming::" lib/` finds nothing
7
- > outside `lib/robot_lab/streaming/`. The class works, but it only publishes what
8
- > *you* hand it. For token streaming from a robot, use `on_content:` or the block
9
- > form of `run`; see the [Streaming overview](index.md#the-real-streaming-api).
10
-
11
- ## Class: `RobotLab::Streaming::Context`
12
-
13
- ```ruby
14
- context = RobotLab::Streaming::Context.new(
15
- run_id: "run_123",
16
- message_id: "msg_456",
17
- scope: "network",
18
- publish: ->(event) { broadcast(event) }
19
- )
20
-
21
- context.publish_event(event: "text.delta", data: { delta: "Hello" })
22
- ```
23
-
24
- ## Constructor
25
-
26
- ```ruby
27
- Context.new(
28
- run_id:,
29
- message_id:,
30
- scope:,
31
- publish:,
32
- parent_run_id: nil,
33
- sequence_counter: nil
34
- )
35
- ```
36
-
37
- **Parameters:**
38
-
39
- | Name | Type | Default | Description |
40
- |------|------|---------|-------------|
41
- | `run_id` | `String` | required | Unique identifier for this run |
42
- | `message_id` | `String` | required | Current message identifier |
43
- | `scope` | `String`, `Symbol` | required | Context scope (e.g., `"network"`, `"robot"`) |
44
- | `publish` | `Proc` | required | Callback invoked with each event hash |
45
- | `parent_run_id` | `String`, `nil` | `nil` | Parent run identifier for nested contexts |
46
- | `sequence_counter` | `SequenceCounter`, `nil` | `nil` | Shared sequence counter (creates new one if nil) |
47
-
48
- ## Attributes
49
-
50
- ### run_id
51
-
52
- ```ruby
53
- context.run_id # => String
54
- ```
55
-
56
- The unique run identifier for this context.
57
-
58
- ### parent_run_id
59
-
60
- ```ruby
61
- context.parent_run_id # => String | nil
62
- ```
63
-
64
- The parent run identifier. Set when this is a child context created by `create_child_context`.
65
-
66
- ### message_id
67
-
68
- ```ruby
69
- context.message_id # => String
70
- ```
71
-
72
- The current message identifier.
73
-
74
- ### scope
75
-
76
- ```ruby
77
- context.scope # => String
78
- ```
79
-
80
- The context scope (converted to string). Typically `"network"` or `"robot"`.
81
-
82
- ## Methods
83
-
84
- ### publish_event
85
-
86
- ```ruby
87
- chunk = context.publish_event(event:, data: {})
88
- ```
89
-
90
- Publish a streaming event. The event is wrapped in a chunk with automatic sequencing, timestamping, and context metadata injection.
91
-
92
- **Parameters:**
93
-
94
- | Name | Type | Description |
95
- |------|------|-------------|
96
- | `event` | `String` | Event type (e.g., `"text.delta"`, `"run.started"`) |
97
- | `data` | `Hash` | Event payload (default: `{}`) |
98
-
99
- **Returns:** The constructed event chunk hash.
100
-
101
- The chunk structure:
102
-
103
- ```ruby
104
- {
105
- event: "text.delta",
106
- data: {
107
- delta: "Hello", # from data parameter
108
- run_id: "run_123", # injected from context
109
- message_id: "msg_456", # injected from context
110
- scope: "robot" # injected from context
111
- },
112
- timestamp: 1707900000000, # millisecond Unix timestamp
113
- sequence_number: 1, # monotonically increasing
114
- id: "publish-1:text.delta" # unique event ID
115
- }
116
- ```
117
-
118
- If the publish callback raises an error, it is caught and logged as a warning via `RobotLab.config.logger`. The chunk is still returned.
119
-
120
- ### create_child_context
121
-
122
- ```ruby
123
- child = context.create_child_context(robot_run_id)
124
- ```
125
-
126
- Create a child context for a nested robot execution. The child shares the same publish callback and sequence counter, ensuring events are ordered globally across the parent and child.
127
-
128
- **Parameters:**
129
-
130
- | Name | Type | Description |
131
- |------|------|-------------|
132
- | `robot_run_id` | `String` | Run ID for the child context |
133
-
134
- **Returns:** A new `Context` with:
135
- - `run_id` set to `robot_run_id`
136
- - `parent_run_id` set to the current context's `run_id`
137
- - `scope` set to `"robot"`
138
- - A new `message_id` (generated UUID)
139
- - Shared `sequence_counter` and `publish` callback
140
-
141
- ### create_context_with_shared_sequence
142
-
143
- ```ruby
144
- sibling = context.create_context_with_shared_sequence(
145
- run_id: "run_789",
146
- message_id: "msg_789",
147
- scope: "robot"
148
- )
149
- ```
150
-
151
- Create a new context that shares the same sequence counter as this context, but with different identifiers.
152
-
153
- **Parameters:**
154
-
155
- | Name | Type | Description |
156
- |------|------|-------------|
157
- | `run_id` | `String` | Run ID for the new context |
158
- | `message_id` | `String` | Message ID for the new context |
159
- | `scope` | `String` | Scope for the new context |
160
-
161
- **Returns:** A new `Context` sharing the sequence counter and publish callback.
162
-
163
- ### generate_part_id
164
-
165
- ```ruby
166
- context.generate_part_id # => "part_37ac1d49_815007_8f522876"
167
- ```
168
-
169
- Generate an OpenAI-compatible part ID (max 40 characters), assembled as
170
- `"part_<msg>_<ts>_<rand>"`:
171
-
172
- | Segment | Source | Length |
173
- |---------|--------|--------|
174
- | `<msg>` | First 8 characters of **`message_id`** (not `run_id`) | 8 |
175
- | `<ts>` | Last 6 digits of the millisecond Unix timestamp | 6 |
176
- | `<rand>` | `SecureRandom.hex(4)` | 8 hex chars |
177
-
178
- Note this reads `message_id`, so two contexts sharing a message produce IDs with
179
- the same first segment.
180
-
181
- ### generate_step_id
182
-
183
- ```ruby
184
- context.generate_step_id("text_output") # => "publish-3:text_output"
185
- ```
186
-
187
- Generate a step ID for durable execution compatibility, formatted as
188
- `"publish-<n>:<base_name>"`. It reads the counter with `current`, which does
189
- **not** increment: `<n>` is the sequence number of the most recently published
190
- event (`0` before anything has been published), so calling it repeatedly between
191
- publishes yields the same ID.
192
-
193
- **Parameters:**
194
-
195
- | Name | Type | Description |
196
- |------|------|-------------|
197
- | `base_name` | `String` | Base name for the step |
198
-
199
- ### generate_message_id
200
-
201
- ```ruby
202
- context.generate_message_id # => "a1b2c3d4-..."
203
- ```
204
-
205
- Generate a new UUID for use as a message identifier.
206
-
207
- ## Examples
208
-
209
- ### Basic Event Publishing
210
-
211
- ```ruby
212
- publish = ->(event) {
213
- puts "[#{event[:event]}] #{event[:data]}"
214
- }
215
-
216
- context = RobotLab::Streaming::Context.new(
217
- run_id: SecureRandom.uuid,
218
- message_id: SecureRandom.uuid,
219
- scope: "robot",
220
- publish: publish
221
- )
222
-
223
- context.publish_event(event: "run.started", data: { robot_name: "assistant" })
224
- context.publish_event(event: "text.delta", data: { delta: "Hello " })
225
- context.publish_event(event: "text.delta", data: { delta: "world!" })
226
- context.publish_event(event: "run.completed", data: {})
227
- ```
228
-
229
- ### Modelling a Multi-Robot Run with Child Contexts
230
-
231
- `RobotLab::Network` does not do any of this for you — the nesting below is
232
- something you would write by hand around your own orchestration.
233
-
234
- ```ruby
235
- # Parent context
236
- network_ctx = RobotLab::Streaming::Context.new(
237
- run_id: "net_run_1",
238
- message_id: "net_msg_1",
239
- scope: "network",
240
- publish: ->(e) { stream_to_client(e) }
241
- )
242
-
243
- network_ctx.publish_event(event: "run.started", data: {})
244
-
245
- # Robot 1 executes
246
- robot1_ctx = network_ctx.create_child_context("robot1_run_1")
247
- robot1_ctx.publish_event(event: "step.started", data: { robot: "classifier" })
248
- robot1_ctx.publish_event(event: "text.delta", data: { delta: "Category: billing" })
249
- robot1_ctx.publish_event(event: "step.completed", data: { robot: "classifier" })
250
-
251
- # Robot 2 executes (sequence numbers continue from robot 1)
252
- robot2_ctx = network_ctx.create_child_context("robot2_run_1")
253
- robot2_ctx.publish_event(event: "step.started", data: { robot: "responder" })
254
- robot2_ctx.publish_event(event: "text.delta", data: { delta: "I can help with that." })
255
- robot2_ctx.publish_event(event: "step.completed", data: { robot: "responder" })
256
-
257
- network_ctx.publish_event(event: "run.completed", data: {})
258
- ```
259
-
260
- ### Error-Safe Publishing
261
-
262
- ```ruby
263
- # Errors in the publish callback are caught and logged,
264
- # so streaming failures do not interrupt execution.
265
- context = RobotLab::Streaming::Context.new(
266
- run_id: "run_1",
267
- message_id: "msg_1",
268
- scope: "robot",
269
- publish: ->(e) { raise "connection lost" }
270
- )
271
-
272
- # This does not raise -- the error is logged via RobotLab.config.logger
273
- chunk = context.publish_event(event: "text.delta", data: { delta: "test" })
274
- # chunk is still returned with the event data
275
- ```
276
-
277
- ## See Also
278
-
279
- - [Streaming Overview](index.md)
280
- - [Events](events.md)