@mseep/open-computer-use 1.0.0

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 (769) hide show
  1. package/.coderabbit.yaml +25 -0
  2. package/.dockerignore +95 -0
  3. package/.env.example +137 -0
  4. package/.githooks/pre-commit +68 -0
  5. package/.github/CODEOWNERS +125 -0
  6. package/.github/ISSUE_TEMPLATE/adr-proposal.md +41 -0
  7. package/.github/ISSUE_TEMPLATE/bug-report.md +49 -0
  8. package/.github/ISSUE_TEMPLATE/component-proposal.md +38 -0
  9. package/.github/ISSUE_TEMPLATE/config.yml +15 -0
  10. package/.github/ISSUE_TEMPLATE/dependency-proposal.md +59 -0
  11. package/.github/ISSUE_TEMPLATE/feature_request.md +15 -0
  12. package/.github/ISSUE_TEMPLATE/nfr-proposal.md +44 -0
  13. package/.github/PULL_REQUEST_TEMPLATE.md +15 -0
  14. package/.github/codeql/codeql-config.yml +11 -0
  15. package/.github/codeql/extensions/security-models/python-sanitizers.model.yml +17 -0
  16. package/.github/codeql/extensions/security-models/qlpack.yml +7 -0
  17. package/.github/dependabot.yml +23 -0
  18. package/.github/security-exceptions.yml +23 -0
  19. package/.github/workflows/build.yml +420 -0
  20. package/.github/workflows/codeql.yml +33 -0
  21. package/.github/workflows/contracts-lint.yml +90 -0
  22. package/.github/workflows/docs-lint.yml +151 -0
  23. package/.github/workflows/helm.yml +131 -0
  24. package/.github/workflows/identity-lint.yml +30 -0
  25. package/.github/workflows/release-chart.yml +177 -0
  26. package/.github/workflows/release.yml +95 -0
  27. package/.github/workflows/security.yml +332 -0
  28. package/.github/workflows/stale.yml +31 -0
  29. package/.github/workflows/supply-chain.yml +242 -0
  30. package/.gitleaks.toml +53 -0
  31. package/.markdownlint.yaml +51 -0
  32. package/.semgrepignore +85 -0
  33. package/.vale/styles/Architecture/ap13-data-class-substrate.yml +12 -0
  34. package/.vale/styles/Architecture/banned-phrases.yml +23 -0
  35. package/.vale/styles/Architecture/banned-vocab.yml +23 -0
  36. package/.vale/styles/Architecture/marketing-tone.yml +19 -0
  37. package/.vale.ini +18 -0
  38. package/CHANGELOG.md +411 -0
  39. package/CLAUDE.md +218 -0
  40. package/CONTRIBUTING.md +82 -0
  41. package/Dockerfile +676 -0
  42. package/LICENSE +98 -0
  43. package/LICENSE-APACHE +202 -0
  44. package/LICENSE-MIT +21 -0
  45. package/NOTICE +36 -0
  46. package/README.md +516 -0
  47. package/SECURITY.md +45 -0
  48. package/THIRD-PARTY-LICENSES.md +14 -0
  49. package/apt-packages.txt +108 -0
  50. package/computer-use-server/.dockerignore +13 -0
  51. package/computer-use-server/Dockerfile +44 -0
  52. package/computer-use-server/README.md +84 -0
  53. package/computer-use-server/app.py +1544 -0
  54. package/computer-use-server/bin/list-subagent-models +449 -0
  55. package/computer-use-server/cli-defaults/README.md +31 -0
  56. package/computer-use-server/cli-defaults/codex.json +7 -0
  57. package/computer-use-server/cli-defaults/opencode.json +18 -0
  58. package/computer-use-server/cli_adapters/__init__.py +46 -0
  59. package/computer-use-server/cli_adapters/claude.py +163 -0
  60. package/computer-use-server/cli_adapters/codex.py +163 -0
  61. package/computer-use-server/cli_adapters/opencode.py +169 -0
  62. package/computer-use-server/cli_adapters/result.py +34 -0
  63. package/computer-use-server/cli_runtime.py +316 -0
  64. package/computer-use-server/context_vars.py +24 -0
  65. package/computer-use-server/docker_manager.py +1100 -0
  66. package/computer-use-server/docs_html.py +12 -0
  67. package/computer-use-server/mcp_resources.py +170 -0
  68. package/computer-use-server/mcp_tools.py +1430 -0
  69. package/computer-use-server/requirements.txt +17 -0
  70. package/computer-use-server/security.py +50 -0
  71. package/computer-use-server/skill_manager.py +664 -0
  72. package/computer-use-server/static/browser-viewer.js +445 -0
  73. package/computer-use-server/static/chart.umd.js +14 -0
  74. package/computer-use-server/static/docs.html +203 -0
  75. package/computer-use-server/static/github-dark.min.css +10 -0
  76. package/computer-use-server/static/github.min.css +10 -0
  77. package/computer-use-server/static/highlight.min.js +1213 -0
  78. package/computer-use-server/static/highlightjs-line-numbers.min.js +1 -0
  79. package/computer-use-server/static/icons.js +74 -0
  80. package/computer-use-server/static/jszip.min.js +13 -0
  81. package/computer-use-server/static/katex/auto-render.min.js +1 -0
  82. package/computer-use-server/static/katex/fonts/KaTeX_AMS-Regular.ttf +0 -0
  83. package/computer-use-server/static/katex/fonts/KaTeX_AMS-Regular.woff +0 -0
  84. package/computer-use-server/static/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  85. package/computer-use-server/static/katex/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  86. package/computer-use-server/static/katex/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  87. package/computer-use-server/static/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  88. package/computer-use-server/static/katex/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  89. package/computer-use-server/static/katex/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  90. package/computer-use-server/static/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  91. package/computer-use-server/static/katex/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  92. package/computer-use-server/static/katex/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  93. package/computer-use-server/static/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  94. package/computer-use-server/static/katex/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  95. package/computer-use-server/static/katex/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  96. package/computer-use-server/static/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  97. package/computer-use-server/static/katex/fonts/KaTeX_Main-Bold.ttf +0 -0
  98. package/computer-use-server/static/katex/fonts/KaTeX_Main-Bold.woff +0 -0
  99. package/computer-use-server/static/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  100. package/computer-use-server/static/katex/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  101. package/computer-use-server/static/katex/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  102. package/computer-use-server/static/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  103. package/computer-use-server/static/katex/fonts/KaTeX_Main-Italic.ttf +0 -0
  104. package/computer-use-server/static/katex/fonts/KaTeX_Main-Italic.woff +0 -0
  105. package/computer-use-server/static/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  106. package/computer-use-server/static/katex/fonts/KaTeX_Main-Regular.ttf +0 -0
  107. package/computer-use-server/static/katex/fonts/KaTeX_Main-Regular.woff +0 -0
  108. package/computer-use-server/static/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  109. package/computer-use-server/static/katex/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  110. package/computer-use-server/static/katex/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  111. package/computer-use-server/static/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  112. package/computer-use-server/static/katex/fonts/KaTeX_Math-Italic.ttf +0 -0
  113. package/computer-use-server/static/katex/fonts/KaTeX_Math-Italic.woff +0 -0
  114. package/computer-use-server/static/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  115. package/computer-use-server/static/katex/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  116. package/computer-use-server/static/katex/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  117. package/computer-use-server/static/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  118. package/computer-use-server/static/katex/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  119. package/computer-use-server/static/katex/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  120. package/computer-use-server/static/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  121. package/computer-use-server/static/katex/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  122. package/computer-use-server/static/katex/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  123. package/computer-use-server/static/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  124. package/computer-use-server/static/katex/fonts/KaTeX_Script-Regular.ttf +0 -0
  125. package/computer-use-server/static/katex/fonts/KaTeX_Script-Regular.woff +0 -0
  126. package/computer-use-server/static/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  127. package/computer-use-server/static/katex/fonts/KaTeX_Size1-Regular.ttf +0 -0
  128. package/computer-use-server/static/katex/fonts/KaTeX_Size1-Regular.woff +0 -0
  129. package/computer-use-server/static/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  130. package/computer-use-server/static/katex/fonts/KaTeX_Size2-Regular.ttf +0 -0
  131. package/computer-use-server/static/katex/fonts/KaTeX_Size2-Regular.woff +0 -0
  132. package/computer-use-server/static/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  133. package/computer-use-server/static/katex/fonts/KaTeX_Size3-Regular.ttf +0 -0
  134. package/computer-use-server/static/katex/fonts/KaTeX_Size3-Regular.woff +0 -0
  135. package/computer-use-server/static/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  136. package/computer-use-server/static/katex/fonts/KaTeX_Size4-Regular.ttf +0 -0
  137. package/computer-use-server/static/katex/fonts/KaTeX_Size4-Regular.woff +0 -0
  138. package/computer-use-server/static/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  139. package/computer-use-server/static/katex/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  140. package/computer-use-server/static/katex/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  141. package/computer-use-server/static/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  142. package/computer-use-server/static/katex/katex.min.css +1 -0
  143. package/computer-use-server/static/katex/katex.min.js +1 -0
  144. package/computer-use-server/static/locale.js +242 -0
  145. package/computer-use-server/static/mammoth.browser.min.js +21 -0
  146. package/computer-use-server/static/marked.min.js +6 -0
  147. package/computer-use-server/static/mermaid.min.js +2811 -0
  148. package/computer-use-server/static/pdf.min.js +22 -0
  149. package/computer-use-server/static/pdf.worker.min.js +22 -0
  150. package/computer-use-server/static/pptxviewjs.min.js +1 -0
  151. package/computer-use-server/static/preact-htm.min.js +1 -0
  152. package/computer-use-server/static/preview.css +1030 -0
  153. package/computer-use-server/static/preview.js +1522 -0
  154. package/computer-use-server/static/xlsx.full.min.js +22 -0
  155. package/computer-use-server/static/xterm-addon-fit.min.js +2 -0
  156. package/computer-use-server/static/xterm-addon-web-links.min.js +2 -0
  157. package/computer-use-server/static/xterm.css +218 -0
  158. package/computer-use-server/static/xterm.min.js +2 -0
  159. package/computer-use-server/system_prompt.py +761 -0
  160. package/computer-use-server/uploads.py +82 -0
  161. package/contracts/README.md +53 -0
  162. package/contracts/audit/audit-fanin.asyncapi.yaml +407 -0
  163. package/contracts/exec/exec-channel.schema.json +240 -0
  164. package/contracts/mcp/2025-06-18/ocu-constraints.schema.json +178 -0
  165. package/contracts/storage/file-artifact-api.schema.json +390 -0
  166. package/contracts/storage/file-ops.schema.json +217 -0
  167. package/contracts/storage/mount-config.schema.json +197 -0
  168. package/cron/Dockerfile +15 -0
  169. package/cron/cleanup-quick.sh +21 -0
  170. package/cron/cleanup.sh +127 -0
  171. package/data/outputs/.gitkeep +0 -0
  172. package/data/uploads/.gitkeep +0 -0
  173. package/docker-compose.test.yml +54 -0
  174. package/docker-compose.webui.yml +77 -0
  175. package/docker-compose.yml +96 -0
  176. package/docs/CLOUD.md +29 -0
  177. package/docs/COMPARISON.md +128 -0
  178. package/docs/DOCKER.md +469 -0
  179. package/docs/DYNAMIC-SKILLS.md +77 -0
  180. package/docs/FEATURES.md +100 -0
  181. package/docs/INSTALL.md +111 -0
  182. package/docs/KNOWN-BUGS.md +86 -0
  183. package/docs/MCP.md +320 -0
  184. package/docs/SCREENSHOTS.md +39 -0
  185. package/docs/SKILLS-USER-GUIDE.md +86 -0
  186. package/docs/SKILLS.md +483 -0
  187. package/docs/TERMINAL-TAB.md +56 -0
  188. package/docs/architecture/02-trust-boundaries.md +224 -0
  189. package/docs/architecture/03-c4-context.md +61 -0
  190. package/docs/architecture/04-bounded-contexts.md +119 -0
  191. package/docs/architecture/05-c4-container.md +88 -0
  192. package/docs/architecture/06-threat-model.md +172 -0
  193. package/docs/architecture/08-contracts.md +105 -0
  194. package/docs/architecture/MANIFESTO.md +38 -0
  195. package/docs/architecture/PROCESS.md +64 -0
  196. package/docs/architecture/README.md +37 -0
  197. package/docs/architecture/adr/0000-template.md +65 -0
  198. package/docs/architecture/adr/0001-layer-0-gate-legacy-exclusion.md +75 -0
  199. package/docs/architecture/adr/0002-session-view-descriptor.md +57 -0
  200. package/docs/architecture/adr/0003-sandbox-runtime-tier-ladder.md +63 -0
  201. package/docs/architecture/adr/0004-operator-authentication-substrate.md +63 -0
  202. package/docs/architecture/adr/0005-egress-credential-delivery-envoy-sds.md +62 -0
  203. package/docs/architecture/adr/0006-egress-forward-proxy-substrate.md +65 -0
  204. package/docs/architecture/adr/0007-egress-auth-mechanism.md +72 -0
  205. package/docs/architecture/adr/0008-session-egress-attribution.md +59 -0
  206. package/docs/architecture/adr/0009-audit-pipeline-pluggable-by-contract.md +76 -0
  207. package/docs/architecture/adr/0010-storage-backend-pluggable-adapter.md +60 -0
  208. package/docs/architecture/adr/0011-storage-egress-lane.md +67 -0
  209. package/docs/architecture/adr/0012-implementation-language.md +67 -0
  210. package/docs/architecture/adr/0020-sandbox-image-provisioning.md +82 -0
  211. package/docs/architecture/adr/README.md +53 -0
  212. package/docs/architecture/compliance/.gitkeep +0 -0
  213. package/docs/architecture/components/00-overview.md +42 -0
  214. package/docs/architecture/components/0000-template.md +50 -0
  215. package/docs/architecture/components/01-mcp-gateway.md +80 -0
  216. package/docs/architecture/components/02-control-operator-api.md +80 -0
  217. package/docs/architecture/components/04-storage-broker.md +104 -0
  218. package/docs/architecture/components/05-session-sandbox.md +93 -0
  219. package/docs/architecture/components/06-egress-trust-edge.md +95 -0
  220. package/docs/architecture/components/07-audit-pipeline.md +110 -0
  221. package/docs/architecture/diagrams/.gitkeep +0 -0
  222. package/docs/architecture/diagrams/02-trust-boundaries.mmd +111 -0
  223. package/docs/architecture/diagrams/06-threat-model.mmd +41 -0
  224. package/docs/architecture/diagrams/08-contracts.mmd +47 -0
  225. package/docs/architecture/diagrams/c4-container.mmd +59 -0
  226. package/docs/architecture/diagrams/c4-context.mmd +46 -0
  227. package/docs/architecture/glossary.md +172 -0
  228. package/docs/architecture/manifesto/.gitkeep +0 -0
  229. package/docs/architecture/manifesto/01-audience-and-buyer.md +57 -0
  230. package/docs/architecture/manifesto/02-nfrs.md +325 -0
  231. package/docs/architecture/manifesto/03-non-negotiables.md +35 -0
  232. package/docs/architecture/manifesto/04-non-goals.md +23 -0
  233. package/docs/architecture/manifesto/05-licensing-posture.md +61 -0
  234. package/docs/architecture/manifesto/06-starter-mode-policy.md +49 -0
  235. package/docs/architecture/manifesto/07-governance.md +60 -0
  236. package/docs/architecture/primitives-backlog.md +51 -0
  237. package/docs/architecture.svg +117 -0
  238. package/docs/claude-code-gateway.md +173 -0
  239. package/docs/cli-config-templates.md +240 -0
  240. package/docs/data-flow.svg +72 -0
  241. package/docs/demo-landing-page.gif +0 -0
  242. package/docs/demo-qwen-trending.gif +0 -0
  243. package/docs/dynamic-skills.svg +77 -0
  244. package/docs/file-flow.svg +126 -0
  245. package/docs/future-architecture/README.md +152 -0
  246. package/docs/future-architecture/adr/0001-control-plane-language-go.md +80 -0
  247. package/docs/future-architecture/adr/0002-guest-agent-language-go.md +84 -0
  248. package/docs/future-architecture/adr/0003-docker-poc-first-then-k8s.md +37 -0
  249. package/docs/future-architecture/adr/0004-pluggable-runtime-via-runtimeclass.md +34 -0
  250. package/docs/future-architecture/adr/0005-mcp-as-control-plane-gateway.md +34 -0
  251. package/docs/future-architecture/adr/0006-no-agpl-no-bsl-dependencies.md +41 -0
  252. package/docs/future-architecture/adr/0007-superseded-by-future-architecture.md +37 -0
  253. package/docs/future-architecture/adr/0008-internal-grpc-external-rest-mcp.md +106 -0
  254. package/docs/future-architecture/adr/0009-external-protocol-dialects.md +94 -0
  255. package/docs/future-architecture/adr/0010-lambda-as-inspiration-not-runtime.md +86 -0
  256. package/docs/future-architecture/adr/0011-kata-as-first-class-dind-runtime.md +84 -0
  257. package/docs/future-architecture/antipatterns.md +552 -0
  258. package/docs/future-architecture/architecture/01-layers.md +109 -0
  259. package/docs/future-architecture/architecture/02-layer4-control-plane.md +122 -0
  260. package/docs/future-architecture/architecture/03-layer3-providers.md +174 -0
  261. package/docs/future-architecture/architecture/04-layer2-runtimes.md +114 -0
  262. package/docs/future-architecture/architecture/04b-credential-broker.md +153 -0
  263. package/docs/future-architecture/architecture/05-layer1-guest-agent.md +138 -0
  264. package/docs/future-architecture/architecture/06-storage.md +134 -0
  265. package/docs/future-architecture/architecture/07-security.md +194 -0
  266. package/docs/future-architecture/architecture/08-networking.md +149 -0
  267. package/docs/future-architecture/architecture/09-templates.md +122 -0
  268. package/docs/future-architecture/architecture/10-observability.md +121 -0
  269. package/docs/future-architecture/design-notes.md +72 -0
  270. package/docs/future-architecture/gaps.md +281 -0
  271. package/docs/future-architecture/phase-template.md +123 -0
  272. package/docs/future-architecture/references.md +225 -0
  273. package/docs/future-architecture/research/01-kata-containers.md +100 -0
  274. package/docs/future-architecture/research/02-e2b-infra.md +133 -0
  275. package/docs/future-architecture/research/03-coder.md +115 -0
  276. package/docs/future-architecture/research/04-cloud-hypervisor.md +99 -0
  277. package/docs/future-architecture/research/05-firecracker.md +114 -0
  278. package/docs/future-architecture/research/06-agent-sandbox.md +142 -0
  279. package/docs/future-architecture/research/07-chromedp.md +78 -0
  280. package/docs/future-architecture/research/08-microsandbox.md +78 -0
  281. package/docs/future-architecture/research/09-agentbox.md +135 -0
  282. package/docs/future-architecture/research/10-sysbox.md +100 -0
  283. package/docs/future-architecture/research/11-firecracker-containerd.md +93 -0
  284. package/docs/future-architecture/research/12-docker-socket-proxy.md +59 -0
  285. package/docs/future-architecture/research/14-e2b-desktop-and-surf.md +107 -0
  286. package/docs/future-architecture/research/18-open-webui-terminals-observed.md +135 -0
  287. package/docs/future-architecture/research/bank-buyer.md +96 -0
  288. package/docs/future-architecture/research/enthusiast-audience.md +106 -0
  289. package/docs/future-architecture/research/proof-uipath-anthropic-2026-05.md +76 -0
  290. package/docs/future-architecture/research/widemoat-thesis-advisor.md +124 -0
  291. package/docs/future-architecture/roadmap.md +438 -0
  292. package/docs/kata-runtime.md +267 -0
  293. package/docs/kubernetes.md +86 -0
  294. package/docs/logo.png +0 -0
  295. package/docs/multi-cli.md +161 -0
  296. package/docs/openwebui-filter.md +134 -0
  297. package/docs/roadmap/implementation-roadmap.md +104 -0
  298. package/docs/sandbox-contents.svg +229 -0
  299. package/docs/screenshots/01-create-document.png +0 -0
  300. package/docs/screenshots/02-file-preview.png +0 -0
  301. package/docs/screenshots/03-browser-viewer.png +0 -0
  302. package/docs/screenshots/04-sub-agent-terminal.png +0 -0
  303. package/docs/screenshots/05-chat-overview.png +0 -0
  304. package/docs/screenshots/06-sub-agent-dashboard.png +0 -0
  305. package/docs/screenshots/07-frontend-design-skill.png +0 -0
  306. package/docs/screenshots/08-pptx-skill.png +0 -0
  307. package/docs/screenshots/09-skill-creator.png +0 -0
  308. package/docs/screenshots/10-data-chart.png +0 -0
  309. package/docs/shared-browser.svg +102 -0
  310. package/docs/system-prompt.md +113 -0
  311. package/docs/terminal-flow.svg +69 -0
  312. package/examples/helm/README.md +20 -0
  313. package/examples/helm/standalone/values.yaml +49 -0
  314. package/examples/helm/with-open-webui/README.md +99 -0
  315. package/examples/helm/with-open-webui/values-computer-use.yaml +32 -0
  316. package/examples/helm/with-open-webui/values-open-webui.yaml +67 -0
  317. package/fonts/NotoEmoji-Regular.ttf +0 -0
  318. package/helm/computer-use-server/.helmignore +17 -0
  319. package/helm/computer-use-server/Chart.yaml +32 -0
  320. package/helm/computer-use-server/README.md +211 -0
  321. package/helm/computer-use-server/templates/NOTES.txt +66 -0
  322. package/helm/computer-use-server/templates/_helpers.tpl +115 -0
  323. package/helm/computer-use-server/templates/configmap-dind-init.yaml +82 -0
  324. package/helm/computer-use-server/templates/configmap.yaml +18 -0
  325. package/helm/computer-use-server/templates/deployment.yaml +248 -0
  326. package/helm/computer-use-server/templates/ingress.yaml +38 -0
  327. package/helm/computer-use-server/templates/networkpolicy.yaml +50 -0
  328. package/helm/computer-use-server/templates/pdb.yaml +16 -0
  329. package/helm/computer-use-server/templates/pvc-data.yaml +20 -0
  330. package/helm/computer-use-server/templates/pvc-skills-cache.yaml +20 -0
  331. package/helm/computer-use-server/templates/pvc-user-data.yaml +20 -0
  332. package/helm/computer-use-server/templates/pvc-var-lib-docker.yaml +27 -0
  333. package/helm/computer-use-server/templates/secret.yaml +23 -0
  334. package/helm/computer-use-server/templates/service.yaml +22 -0
  335. package/helm/computer-use-server/templates/serviceaccount.yaml +15 -0
  336. package/helm/computer-use-server/templates/tests/test-health.yaml +23 -0
  337. package/helm/computer-use-server/values.schema.json +183 -0
  338. package/helm/computer-use-server/values.yaml +297 -0
  339. package/lychee.toml +36 -0
  340. package/openwebui/Dockerfile +52 -0
  341. package/openwebui/README.md +38 -0
  342. package/openwebui/functions/README.md +48 -0
  343. package/openwebui/functions/computer_link_filter.py +487 -0
  344. package/openwebui/init.sh +305 -0
  345. package/openwebui/patches/README.md +44 -0
  346. package/openwebui/patches/fix_artifacts_auto_show.py +441 -0
  347. package/openwebui/patches/fix_attached_files_position.py +87 -0
  348. package/openwebui/patches/fix_large_tool_args.py +156 -0
  349. package/openwebui/patches/fix_large_tool_results.py +289 -0
  350. package/openwebui/patches/fix_preview_url_detection.py +230 -0
  351. package/openwebui/patches/fix_skip_embedding_chat_files.py +229 -0
  352. package/openwebui/patches/fix_skip_rag_files_native_fc.py +100 -0
  353. package/openwebui/patches/fix_tool_loop_errors.py +510 -0
  354. package/package.json +39 -0
  355. package/requirements.txt +112 -0
  356. package/scripts/check-config.sh +141 -0
  357. package/scripts/docs-lint/ai-slop-detector.sh +202 -0
  358. package/scripts/docs-lint/architecture-tree-whitelist.sh +131 -0
  359. package/scripts/docs-lint/ascii-diagram-detector.sh +58 -0
  360. package/scripts/docs-lint/front-matter-validator.sh +97 -0
  361. package/scripts/docs-lint/gitignored-ref-detector.sh +122 -0
  362. package/scripts/docs-lint/identity-email-detector.sh +48 -0
  363. package/scripts/docs-lint/test-linters.sh +354 -0
  364. package/scripts/docs-lint/wc-budget.sh +61 -0
  365. package/scripts/githooks/pre-push +75 -0
  366. package/server.json +13 -0
  367. package/settings-wrapper/Dockerfile +9 -0
  368. package/settings-wrapper/README.md +119 -0
  369. package/settings-wrapper/app.py +113 -0
  370. package/settings-wrapper/requirements.txt +2 -0
  371. package/settings-wrapper/skills.json +25 -0
  372. package/skills/README.md +46 -0
  373. package/skills/examples/algorithmic-art/SKILL.md +405 -0
  374. package/skills/examples/algorithmic-art/templates/generator_template.js +223 -0
  375. package/skills/examples/algorithmic-art/templates/viewer.html +601 -0
  376. package/skills/examples/artifacts-builder/SKILL.md +74 -0
  377. package/skills/examples/artifacts-builder/scripts/bundle-artifact.sh +54 -0
  378. package/skills/examples/artifacts-builder/scripts/init-artifact.sh +322 -0
  379. package/skills/examples/artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
  380. package/skills/examples/canvas-design/LICENSE.txt +202 -0
  381. package/skills/examples/canvas-design/SKILL.md +130 -0
  382. package/skills/examples/canvas-design/canvas-fonts/ArsenalSC-OFL.txt +93 -0
  383. package/skills/examples/canvas-design/canvas-fonts/ArsenalSC-Regular.ttf +0 -0
  384. package/skills/examples/canvas-design/canvas-fonts/BigShoulders-Bold.ttf +0 -0
  385. package/skills/examples/canvas-design/canvas-fonts/BigShoulders-OFL.txt +93 -0
  386. package/skills/examples/canvas-design/canvas-fonts/BigShoulders-Regular.ttf +0 -0
  387. package/skills/examples/canvas-design/canvas-fonts/Boldonse-OFL.txt +93 -0
  388. package/skills/examples/canvas-design/canvas-fonts/Boldonse-Regular.ttf +0 -0
  389. package/skills/examples/canvas-design/canvas-fonts/BricolageGrotesque-Bold.ttf +0 -0
  390. package/skills/examples/canvas-design/canvas-fonts/BricolageGrotesque-OFL.txt +93 -0
  391. package/skills/examples/canvas-design/canvas-fonts/BricolageGrotesque-Regular.ttf +0 -0
  392. package/skills/examples/canvas-design/canvas-fonts/CrimsonPro-Bold.ttf +0 -0
  393. package/skills/examples/canvas-design/canvas-fonts/CrimsonPro-Italic.ttf +0 -0
  394. package/skills/examples/canvas-design/canvas-fonts/CrimsonPro-OFL.txt +93 -0
  395. package/skills/examples/canvas-design/canvas-fonts/CrimsonPro-Regular.ttf +0 -0
  396. package/skills/examples/canvas-design/canvas-fonts/DMMono-OFL.txt +93 -0
  397. package/skills/examples/canvas-design/canvas-fonts/DMMono-Regular.ttf +0 -0
  398. package/skills/examples/canvas-design/canvas-fonts/EricaOne-OFL.txt +94 -0
  399. package/skills/examples/canvas-design/canvas-fonts/EricaOne-Regular.ttf +0 -0
  400. package/skills/examples/canvas-design/canvas-fonts/GeistMono-Bold.ttf +0 -0
  401. package/skills/examples/canvas-design/canvas-fonts/GeistMono-OFL.txt +93 -0
  402. package/skills/examples/canvas-design/canvas-fonts/GeistMono-Regular.ttf +0 -0
  403. package/skills/examples/canvas-design/canvas-fonts/Gloock-OFL.txt +93 -0
  404. package/skills/examples/canvas-design/canvas-fonts/Gloock-Regular.ttf +0 -0
  405. package/skills/examples/canvas-design/canvas-fonts/IBMPlexMono-Bold.ttf +0 -0
  406. package/skills/examples/canvas-design/canvas-fonts/IBMPlexMono-OFL.txt +93 -0
  407. package/skills/examples/canvas-design/canvas-fonts/IBMPlexMono-Regular.ttf +0 -0
  408. package/skills/examples/canvas-design/canvas-fonts/IBMPlexSerif-Bold.ttf +0 -0
  409. package/skills/examples/canvas-design/canvas-fonts/IBMPlexSerif-BoldItalic.ttf +0 -0
  410. package/skills/examples/canvas-design/canvas-fonts/IBMPlexSerif-Italic.ttf +0 -0
  411. package/skills/examples/canvas-design/canvas-fonts/IBMPlexSerif-Regular.ttf +0 -0
  412. package/skills/examples/canvas-design/canvas-fonts/InstrumentSans-Bold.ttf +0 -0
  413. package/skills/examples/canvas-design/canvas-fonts/InstrumentSans-BoldItalic.ttf +0 -0
  414. package/skills/examples/canvas-design/canvas-fonts/InstrumentSans-Italic.ttf +0 -0
  415. package/skills/examples/canvas-design/canvas-fonts/InstrumentSans-OFL.txt +93 -0
  416. package/skills/examples/canvas-design/canvas-fonts/InstrumentSans-Regular.ttf +0 -0
  417. package/skills/examples/canvas-design/canvas-fonts/InstrumentSerif-Italic.ttf +0 -0
  418. package/skills/examples/canvas-design/canvas-fonts/InstrumentSerif-Regular.ttf +0 -0
  419. package/skills/examples/canvas-design/canvas-fonts/Italiana-OFL.txt +93 -0
  420. package/skills/examples/canvas-design/canvas-fonts/Italiana-Regular.ttf +0 -0
  421. package/skills/examples/canvas-design/canvas-fonts/JetBrainsMono-Bold.ttf +0 -0
  422. package/skills/examples/canvas-design/canvas-fonts/JetBrainsMono-OFL.txt +93 -0
  423. package/skills/examples/canvas-design/canvas-fonts/JetBrainsMono-Regular.ttf +0 -0
  424. package/skills/examples/canvas-design/canvas-fonts/Jura-Light.ttf +0 -0
  425. package/skills/examples/canvas-design/canvas-fonts/Jura-Medium.ttf +0 -0
  426. package/skills/examples/canvas-design/canvas-fonts/Jura-OFL.txt +93 -0
  427. package/skills/examples/canvas-design/canvas-fonts/LibreBaskerville-OFL.txt +93 -0
  428. package/skills/examples/canvas-design/canvas-fonts/LibreBaskerville-Regular.ttf +0 -0
  429. package/skills/examples/canvas-design/canvas-fonts/Lora-Bold.ttf +0 -0
  430. package/skills/examples/canvas-design/canvas-fonts/Lora-BoldItalic.ttf +0 -0
  431. package/skills/examples/canvas-design/canvas-fonts/Lora-Italic.ttf +0 -0
  432. package/skills/examples/canvas-design/canvas-fonts/Lora-OFL.txt +93 -0
  433. package/skills/examples/canvas-design/canvas-fonts/Lora-Regular.ttf +0 -0
  434. package/skills/examples/canvas-design/canvas-fonts/NationalPark-Bold.ttf +0 -0
  435. package/skills/examples/canvas-design/canvas-fonts/NationalPark-OFL.txt +93 -0
  436. package/skills/examples/canvas-design/canvas-fonts/NationalPark-Regular.ttf +0 -0
  437. package/skills/examples/canvas-design/canvas-fonts/NothingYouCouldDo-OFL.txt +93 -0
  438. package/skills/examples/canvas-design/canvas-fonts/NothingYouCouldDo-Regular.ttf +0 -0
  439. package/skills/examples/canvas-design/canvas-fonts/Outfit-Bold.ttf +0 -0
  440. package/skills/examples/canvas-design/canvas-fonts/Outfit-OFL.txt +93 -0
  441. package/skills/examples/canvas-design/canvas-fonts/Outfit-Regular.ttf +0 -0
  442. package/skills/examples/canvas-design/canvas-fonts/PixelifySans-Medium.ttf +0 -0
  443. package/skills/examples/canvas-design/canvas-fonts/PixelifySans-OFL.txt +93 -0
  444. package/skills/examples/canvas-design/canvas-fonts/PoiretOne-OFL.txt +93 -0
  445. package/skills/examples/canvas-design/canvas-fonts/PoiretOne-Regular.ttf +0 -0
  446. package/skills/examples/canvas-design/canvas-fonts/RedHatMono-Bold.ttf +0 -0
  447. package/skills/examples/canvas-design/canvas-fonts/RedHatMono-OFL.txt +93 -0
  448. package/skills/examples/canvas-design/canvas-fonts/RedHatMono-Regular.ttf +0 -0
  449. package/skills/examples/canvas-design/canvas-fonts/Silkscreen-OFL.txt +93 -0
  450. package/skills/examples/canvas-design/canvas-fonts/Silkscreen-Regular.ttf +0 -0
  451. package/skills/examples/canvas-design/canvas-fonts/SmoochSans-Medium.ttf +0 -0
  452. package/skills/examples/canvas-design/canvas-fonts/SmoochSans-OFL.txt +93 -0
  453. package/skills/examples/canvas-design/canvas-fonts/Tektur-Medium.ttf +0 -0
  454. package/skills/examples/canvas-design/canvas-fonts/Tektur-OFL.txt +93 -0
  455. package/skills/examples/canvas-design/canvas-fonts/Tektur-Regular.ttf +0 -0
  456. package/skills/examples/canvas-design/canvas-fonts/WorkSans-Bold.ttf +0 -0
  457. package/skills/examples/canvas-design/canvas-fonts/WorkSans-BoldItalic.ttf +0 -0
  458. package/skills/examples/canvas-design/canvas-fonts/WorkSans-Italic.ttf +0 -0
  459. package/skills/examples/canvas-design/canvas-fonts/WorkSans-OFL.txt +93 -0
  460. package/skills/examples/canvas-design/canvas-fonts/WorkSans-Regular.ttf +0 -0
  461. package/skills/examples/canvas-design/canvas-fonts/YoungSerif-OFL.txt +93 -0
  462. package/skills/examples/canvas-design/canvas-fonts/YoungSerif-Regular.ttf +0 -0
  463. package/skills/examples/copy-editing/SKILL.md +447 -0
  464. package/skills/examples/copy-editing/evals/evals.json +89 -0
  465. package/skills/examples/copy-editing/references/plain-english-alternatives.md +394 -0
  466. package/skills/examples/internal-comms/LICENSE.txt +202 -0
  467. package/skills/examples/internal-comms/SKILL.md +32 -0
  468. package/skills/examples/internal-comms/examples/3p-updates.md +47 -0
  469. package/skills/examples/internal-comms/examples/company-newsletter.md +65 -0
  470. package/skills/examples/internal-comms/examples/faq-answers.md +30 -0
  471. package/skills/examples/internal-comms/examples/general-comms.md +16 -0
  472. package/skills/examples/mcp-builder/SKILL.md +328 -0
  473. package/skills/examples/mcp-builder/reference/evaluation.md +602 -0
  474. package/skills/examples/mcp-builder/reference/mcp_best_practices.md +915 -0
  475. package/skills/examples/mcp-builder/reference/node_mcp_server.md +916 -0
  476. package/skills/examples/mcp-builder/reference/python_mcp_server.md +752 -0
  477. package/skills/examples/mcp-builder/scripts/connections.py +151 -0
  478. package/skills/examples/mcp-builder/scripts/evaluation.py +373 -0
  479. package/skills/examples/mcp-builder/scripts/example_evaluation.xml +22 -0
  480. package/skills/examples/mcp-builder/scripts/requirements.txt +2 -0
  481. package/skills/examples/product-marketing-context/SKILL.md +241 -0
  482. package/skills/examples/product-marketing-context/evals/evals.json +85 -0
  483. package/skills/examples/single-cell-rna-qc/SKILL.md +175 -0
  484. package/skills/examples/single-cell-rna-qc/references/scverse_qc_guidelines.md +186 -0
  485. package/skills/examples/single-cell-rna-qc/scripts/qc_analysis.py +232 -0
  486. package/skills/examples/single-cell-rna-qc/scripts/qc_core.py +233 -0
  487. package/skills/examples/single-cell-rna-qc/scripts/qc_plotting.py +235 -0
  488. package/skills/examples/skill-creator/SKILL.md +355 -0
  489. package/skills/examples/skill-creator/references/output-patterns.md +82 -0
  490. package/skills/examples/skill-creator/references/workflows.md +28 -0
  491. package/skills/examples/skill-creator/scripts/init_skill.py +303 -0
  492. package/skills/examples/skill-creator/scripts/package_skill.py +110 -0
  493. package/skills/examples/skill-creator/scripts/quick_validate.py +95 -0
  494. package/skills/examples/slack-gif-creator/SKILL.md +254 -0
  495. package/skills/examples/slack-gif-creator/core/easing.py +234 -0
  496. package/skills/examples/slack-gif-creator/core/frame_composer.py +176 -0
  497. package/skills/examples/slack-gif-creator/core/gif_builder.py +269 -0
  498. package/skills/examples/slack-gif-creator/core/validators.py +136 -0
  499. package/skills/examples/slack-gif-creator/requirements.txt +4 -0
  500. package/skills/examples/social-content/SKILL.md +278 -0
  501. package/skills/examples/social-content/evals/evals.json +92 -0
  502. package/skills/examples/social-content/references/platforms.md +170 -0
  503. package/skills/examples/social-content/references/post-templates.md +177 -0
  504. package/skills/examples/social-content/references/reverse-engineering.md +195 -0
  505. package/skills/examples/theme-factory/SKILL.md +59 -0
  506. package/skills/examples/theme-factory/theme-showcase.pdf +0 -0
  507. package/skills/examples/theme-factory/themes/arctic-frost.md +19 -0
  508. package/skills/examples/theme-factory/themes/botanical-garden.md +19 -0
  509. package/skills/examples/theme-factory/themes/desert-rose.md +19 -0
  510. package/skills/examples/theme-factory/themes/forest-canopy.md +19 -0
  511. package/skills/examples/theme-factory/themes/golden-hour.md +19 -0
  512. package/skills/examples/theme-factory/themes/midnight-galaxy.md +19 -0
  513. package/skills/examples/theme-factory/themes/modern-minimalist.md +19 -0
  514. package/skills/examples/theme-factory/themes/ocean-depths.md +19 -0
  515. package/skills/examples/theme-factory/themes/sunset-boulevard.md +19 -0
  516. package/skills/examples/theme-factory/themes/tech-innovation.md +19 -0
  517. package/skills/examples/web-artifacts-builder/LICENSE.txt +202 -0
  518. package/skills/examples/web-artifacts-builder/SKILL.md +74 -0
  519. package/skills/examples/web-artifacts-builder/scripts/bundle-artifact.sh +54 -0
  520. package/skills/examples/web-artifacts-builder/scripts/init-artifact.sh +322 -0
  521. package/skills/examples/web-artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
  522. package/skills/examples/writing-skills/SKILL.md +655 -0
  523. package/skills/examples/writing-skills/anthropic-best-practices.md +1150 -0
  524. package/skills/examples/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
  525. package/skills/examples/writing-skills/graphviz-conventions.dot +172 -0
  526. package/skills/examples/writing-skills/persuasion-principles.md +187 -0
  527. package/skills/examples/writing-skills/render-graphs.js +168 -0
  528. package/skills/examples/writing-skills/testing-skills-with-subagents.md +384 -0
  529. package/skills/public/describe-image/SKILL.md +105 -0
  530. package/skills/public/describe-image/scripts/describe.py +389 -0
  531. package/skills/public/doc-coauthoring/SKILL.md +375 -0
  532. package/skills/public/docx/LICENSE.txt +30 -0
  533. package/skills/public/docx/SKILL.md +199 -0
  534. package/skills/public/docx/docx-js.md +350 -0
  535. package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  536. package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  537. package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  538. package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  539. package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  540. package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  541. package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  542. package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  543. package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  544. package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  545. package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  546. package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  547. package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  548. package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  549. package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  550. package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  551. package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  552. package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  553. package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  554. package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  555. package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  556. package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  557. package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  558. package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  559. package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  560. package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  561. package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  562. package/skills/public/docx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  563. package/skills/public/docx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  564. package/skills/public/docx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  565. package/skills/public/docx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  566. package/skills/public/docx/ooxml/schemas/mce/mc.xsd +75 -0
  567. package/skills/public/docx/ooxml/schemas/microsoft/wml-2010.xsd +560 -0
  568. package/skills/public/docx/ooxml/schemas/microsoft/wml-2012.xsd +67 -0
  569. package/skills/public/docx/ooxml/schemas/microsoft/wml-2018.xsd +14 -0
  570. package/skills/public/docx/ooxml/schemas/microsoft/wml-cex-2018.xsd +20 -0
  571. package/skills/public/docx/ooxml/schemas/microsoft/wml-cid-2016.xsd +13 -0
  572. package/skills/public/docx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  573. package/skills/public/docx/ooxml/schemas/microsoft/wml-symex-2015.xsd +8 -0
  574. package/skills/public/docx/ooxml/scripts/pack.py +159 -0
  575. package/skills/public/docx/ooxml/scripts/unpack.py +29 -0
  576. package/skills/public/docx/ooxml/scripts/validate.py +69 -0
  577. package/skills/public/docx/ooxml/scripts/validation/__init__.py +15 -0
  578. package/skills/public/docx/ooxml/scripts/validation/base.py +951 -0
  579. package/skills/public/docx/ooxml/scripts/validation/docx.py +274 -0
  580. package/skills/public/docx/ooxml/scripts/validation/pptx.py +315 -0
  581. package/skills/public/docx/ooxml/scripts/validation/redlining.py +279 -0
  582. package/skills/public/docx/ooxml.md +632 -0
  583. package/skills/public/docx/scripts/__init__.py +1 -0
  584. package/skills/public/docx/scripts/document.py +1292 -0
  585. package/skills/public/docx/scripts/templates/comments.xml +3 -0
  586. package/skills/public/docx/scripts/templates/commentsExtended.xml +3 -0
  587. package/skills/public/docx/scripts/templates/commentsExtensible.xml +3 -0
  588. package/skills/public/docx/scripts/templates/commentsIds.xml +3 -0
  589. package/skills/public/docx/scripts/templates/people.xml +3 -0
  590. package/skills/public/docx/scripts/utilities.py +374 -0
  591. package/skills/public/file-reading/LICENSE.txt +30 -0
  592. package/skills/public/file-reading/SKILL.md +350 -0
  593. package/skills/public/frontend-design/LICENSE.txt +177 -0
  594. package/skills/public/frontend-design/SKILL.md +42 -0
  595. package/skills/public/gitlab-explorer/SKILL.md +174 -0
  596. package/skills/public/gitlab-explorer/references/git-commands.md +323 -0
  597. package/skills/public/gitlab-explorer/references/glab-commands.md +282 -0
  598. package/skills/public/gitlab-explorer/scripts/check_gitlab_auth.sh +109 -0
  599. package/skills/public/pdf/FORMS.md +205 -0
  600. package/skills/public/pdf/REFERENCE.md +612 -0
  601. package/skills/public/pdf/SKILL.md +364 -0
  602. package/skills/public/pdf/scripts/check_bounding_boxes.py +70 -0
  603. package/skills/public/pdf/scripts/check_bounding_boxes_test.py +226 -0
  604. package/skills/public/pdf/scripts/check_fillable_fields.py +12 -0
  605. package/skills/public/pdf/scripts/convert_pdf_to_images.py +35 -0
  606. package/skills/public/pdf/scripts/create_validation_image.py +41 -0
  607. package/skills/public/pdf/scripts/extract_form_field_info.py +152 -0
  608. package/skills/public/pdf/scripts/fill_fillable_fields.py +114 -0
  609. package/skills/public/pdf/scripts/fill_pdf_form_with_annotations.py +108 -0
  610. package/skills/public/pdf-reading/LICENSE.txt +30 -0
  611. package/skills/public/pdf-reading/REFERENCE.md +196 -0
  612. package/skills/public/pdf-reading/SKILL.md +305 -0
  613. package/skills/public/playwright-cli/SKILL.md +278 -0
  614. package/skills/public/playwright-cli/references/request-mocking.md +87 -0
  615. package/skills/public/playwright-cli/references/running-code.md +232 -0
  616. package/skills/public/playwright-cli/references/session-management.md +169 -0
  617. package/skills/public/playwright-cli/references/storage-state.md +275 -0
  618. package/skills/public/playwright-cli/references/test-generation.md +88 -0
  619. package/skills/public/playwright-cli/references/tracing.md +139 -0
  620. package/skills/public/playwright-cli/references/video-recording.md +43 -0
  621. package/skills/public/pptx/LICENSE.txt +30 -0
  622. package/skills/public/pptx/SKILL.md +484 -0
  623. package/skills/public/pptx/css.md +335 -0
  624. package/skills/public/pptx/html2pptx.md +893 -0
  625. package/skills/public/pptx/html2pptx.tgz +0 -0
  626. package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  627. package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  628. package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  629. package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  630. package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  631. package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  632. package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  633. package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  634. package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  635. package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  636. package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  637. package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  638. package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  639. package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  640. package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  641. package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  642. package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  643. package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  644. package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  645. package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  646. package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  647. package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  648. package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  649. package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  650. package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  651. package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  652. package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  653. package/skills/public/pptx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  654. package/skills/public/pptx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  655. package/skills/public/pptx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  656. package/skills/public/pptx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  657. package/skills/public/pptx/ooxml/schemas/mce/mc.xsd +75 -0
  658. package/skills/public/pptx/ooxml/schemas/microsoft/wml-2010.xsd +560 -0
  659. package/skills/public/pptx/ooxml/schemas/microsoft/wml-2012.xsd +67 -0
  660. package/skills/public/pptx/ooxml/schemas/microsoft/wml-2018.xsd +14 -0
  661. package/skills/public/pptx/ooxml/schemas/microsoft/wml-cex-2018.xsd +20 -0
  662. package/skills/public/pptx/ooxml/schemas/microsoft/wml-cid-2016.xsd +13 -0
  663. package/skills/public/pptx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  664. package/skills/public/pptx/ooxml/schemas/microsoft/wml-symex-2015.xsd +8 -0
  665. package/skills/public/pptx/ooxml/scripts/pack.py +159 -0
  666. package/skills/public/pptx/ooxml/scripts/unpack.py +29 -0
  667. package/skills/public/pptx/ooxml/scripts/validate.py +69 -0
  668. package/skills/public/pptx/ooxml/scripts/validation/__init__.py +15 -0
  669. package/skills/public/pptx/ooxml/scripts/validation/base.py +951 -0
  670. package/skills/public/pptx/ooxml/scripts/validation/docx.py +274 -0
  671. package/skills/public/pptx/ooxml/scripts/validation/pptx.py +315 -0
  672. package/skills/public/pptx/ooxml/scripts/validation/redlining.py +279 -0
  673. package/skills/public/pptx/ooxml.md +427 -0
  674. package/skills/public/pptx/scripts/inventory.py +1020 -0
  675. package/skills/public/pptx/scripts/rearrange.py +231 -0
  676. package/skills/public/pptx/scripts/replace.py +385 -0
  677. package/skills/public/pptx/scripts/thumbnail.py +450 -0
  678. package/skills/public/skill-creator/SKILL.md +356 -0
  679. package/skills/public/skill-creator/references/output-patterns.md +82 -0
  680. package/skills/public/skill-creator/references/workflows.md +28 -0
  681. package/skills/public/skill-creator/scripts/init_skill.py +303 -0
  682. package/skills/public/skill-creator/scripts/package_skill.py +110 -0
  683. package/skills/public/skill-creator/scripts/quick_validate.py +95 -0
  684. package/skills/public/sub-agent/SKILL.md +186 -0
  685. package/skills/public/sub-agent/references/security-review.md +153 -0
  686. package/skills/public/sub-agent/references/usage.md +207 -0
  687. package/skills/public/sub-agent/scripts/list_subagent_models.sh +22 -0
  688. package/skills/public/test-driven-development/SKILL.md +371 -0
  689. package/skills/public/test-driven-development/testing-anti-patterns.md +299 -0
  690. package/skills/public/webapp-testing/LICENSE.txt +202 -0
  691. package/skills/public/webapp-testing/SKILL.md +96 -0
  692. package/skills/public/webapp-testing/examples/console_logging.py +35 -0
  693. package/skills/public/webapp-testing/examples/element_discovery.py +40 -0
  694. package/skills/public/webapp-testing/examples/static_html_automation.py +33 -0
  695. package/skills/public/webapp-testing/scripts/with_server.py +106 -0
  696. package/skills/public/xlsx/LICENSE.txt +30 -0
  697. package/skills/public/xlsx/SKILL.md +316 -0
  698. package/skills/public/xlsx/preview_data.py +93 -0
  699. package/skills/public/xlsx/recalc.py +178 -0
  700. package/tests/README.md +42 -0
  701. package/tests/fixtures/cli/claude_v0.9.2.0_argv.json +46 -0
  702. package/tests/fixtures/cli/claude_v0.9.2.0_stdout.json +32 -0
  703. package/tests/fixtures/cli/codex_run.jsonl +4 -0
  704. package/tests/fixtures/cli/opencode_run.jsonl +6 -0
  705. package/tests/integration/README.md +56 -0
  706. package/tests/integration/conftest.py +280 -0
  707. package/tests/integration/pytest.ini +13 -0
  708. package/tests/integration/test_mcp_auth.py +85 -0
  709. package/tests/integration/test_mcp_tools.py +101 -0
  710. package/tests/integration/test_workspace_lifecycle.py +125 -0
  711. package/tests/orchestrator/mock_llm_server.py +343 -0
  712. package/tests/orchestrator/test_cli_adapters.py +566 -0
  713. package/tests/orchestrator/test_cli_adapters_live.py +527 -0
  714. package/tests/orchestrator/test_cli_runtime.py +451 -0
  715. package/tests/orchestrator/test_docker_manager.py +302 -0
  716. package/tests/orchestrator/test_dynamic_instructions.py +69 -0
  717. package/tests/orchestrator/test_mcp_resources.py +140 -0
  718. package/tests/orchestrator/test_mcp_tools.py +224 -0
  719. package/tests/orchestrator/test_passthrough_isolation.py +201 -0
  720. package/tests/orchestrator/test_readme_in_container.py +76 -0
  721. package/tests/orchestrator/test_render_cache.py +84 -0
  722. package/tests/orchestrator/test_runtime_cli_endpoint.py +108 -0
  723. package/tests/orchestrator/test_single_user_mode.py +212 -0
  724. package/tests/orchestrator/test_startup_warnings.py +123 -0
  725. package/tests/orchestrator/test_sub_agent_dispatch.py +327 -0
  726. package/tests/orchestrator/test_subagent_claude_compat.py +367 -0
  727. package/tests/orchestrator/test_system_prompt_endpoint.py +191 -0
  728. package/tests/orchestrator/test_tool_descriptions.py +52 -0
  729. package/tests/orchestrator/test_view_image.py +201 -0
  730. package/tests/patches/conftest.py +30 -0
  731. package/tests/patches/fixtures/__init__.py +10 -0
  732. package/tests/patches/fixtures/middleware_v0.9.1.py +5057 -0
  733. package/tests/patches/fixtures/middleware_v0.9.2.py +5120 -0
  734. package/tests/patches/fixtures/retrieval_v0.9.1.py +2684 -0
  735. package/tests/patches/fixtures/retrieval_v0.9.2.py +2700 -0
  736. package/tests/patches/test_fix_attached_files_position.py +118 -0
  737. package/tests/patches/test_fix_large_tool_args.py +130 -0
  738. package/tests/patches/test_fix_large_tool_results.py +531 -0
  739. package/tests/patches/test_fix_skip_embedding_chat_files.py +160 -0
  740. package/tests/patches/test_fix_skip_rag_files_native_fc.py +120 -0
  741. package/tests/patches/test_fix_tool_loop_errors.py +128 -0
  742. package/tests/security/test_path_traversal_app.py +132 -0
  743. package/tests/security/test_path_traversal_docker.py +36 -0
  744. package/tests/security/test_path_traversal_settings.py +87 -0
  745. package/tests/security/test_safe_path_util.py +166 -0
  746. package/tests/security/test_xss_preview.py +46 -0
  747. package/tests/test-default-model-resolution.py +136 -0
  748. package/tests/test-docker-image.sh +358 -0
  749. package/tests/test-list-subagent-models.sh +421 -0
  750. package/tests/test-mcp-endpoint-live.sh +92 -0
  751. package/tests/test-mcp-native-surface.sh +213 -0
  752. package/tests/test-no-cyrillic.sh +135 -0
  753. package/tests/test-opencode-error-mapping.py +130 -0
  754. package/tests/test-pr88-skills.sh +305 -0
  755. package/tests/test-project-structure.sh +202 -0
  756. package/tests/test-single-user-mode.sh +269 -0
  757. package/tests/test-skill-no-hardcoded-models.sh +65 -0
  758. package/tests/test-subagent-cli-surface.py +137 -0
  759. package/tests/test-subagent-runtime.sh +109 -0
  760. package/tests/test_codex_toml_converter.py +204 -0
  761. package/tests/test_default_resolver_no_legacy_global.py +159 -0
  762. package/tests/test_filter.py +648 -0
  763. package/tests/test_init_sh_unchanged.sh +49 -0
  764. package/tests/test_opencode_alias_map_drop.py +144 -0
  765. package/tests/test_requirements.py +91 -0
  766. package/tests/test_subagent_docstring.py +193 -0
  767. package/tests/test_tools.py +34 -0
  768. package/vendor/extract-text/README.md +46 -0
  769. package/vendor/extract-text/extract-text +0 -0
@@ -0,0 +1,1430 @@
1
+ # SPDX-License-Identifier: FSL-1.1-Apache-2.0
2
+ # Copyright (c) 2025 Open Computer Use Contributors
3
+ """
4
+ MCP Server Tools for Computer Use
5
+
6
+ Provides MCP tools (bash, str_replace, create_file, view, sub_agent) via Streamable HTTP.
7
+ Works with local Docker socket for container management.
8
+
9
+ ARCHITECTURE:
10
+ - File-server runs alongside Docker daemon on the same host
11
+ - Docker containers are created/managed via local Docker socket
12
+ - Each chat gets its own isolated container: owui-chat-{chat_id}
13
+
14
+ GITLAB TOKEN FETCHING:
15
+ Priority order for GitLab token:
16
+ 1. X-Gitlab-Token header (direct from client)
17
+ 2. MCP Tokens Wrapper (fetches token by user email)
18
+ 3. No token (continue without GitLab auth)
19
+
20
+ HTTP Headers — all optional except Chat ID. Headers override env var defaults.
21
+
22
+ | Parameter | Header | Alt Header (OpenWebUI) | Required | Fallback |
23
+ |-------------------|------------------------|--------------------------------|----------|---------------------------------|
24
+ | Chat ID | X-Chat-Id | X-OpenWebUI-Chat-Id | Yes | — |
25
+ | User Email | X-User-Email | X-OpenWebUI-User-Email | No | — |
26
+ | User Name | X-User-Name | X-OpenWebUI-User-Name | No | — |
27
+ | GitLab Token | X-Gitlab-Token | X-OpenWebUI-Gitlab-Token | No | MCP Tokens Wrapper by email |
28
+ | GitLab Host | X-Gitlab-Host | X-OpenWebUI-Gitlab-Host | No | gitlab.com |
29
+ | Anthropic API Key | X-Anthropic-Api-Key | X-OpenWebUI-Anthropic-Api-Key | No | ANTHROPIC_AUTH_TOKEN env |
30
+ | Anthropic Base URL| X-Anthropic-Base-Url | X-OpenWebUI-Anthropic-Base-Url | No | ANTHROPIC_BASE_URL env |
31
+ | MCP Tokens URL | X-Mcp-Tokens-Url | X-OpenWebUI-Mcp-Tokens-Url | No | MCP_TOKENS_URL env |
32
+ | MCP Tokens API Key| X-Mcp-Tokens-Api-Key | X-OpenWebUI-Mcp-Tokens-Api-Key | No | MCP_TOKENS_API_KEY env |
33
+ | MCP Servers | X-Mcp-Servers | X-OpenWebUI-Mcp-Servers | No | — |
34
+
35
+ Environment Variables (computer-use-orchestrator defaults):
36
+ - MCP_TOKENS_URL: MCP Tokens Wrapper service (optional, for centralized token management)
37
+ - MCP_TOKENS_API_KEY: Internal API key for MCP Tokens Wrapper
38
+ - ANTHROPIC_AUTH_TOKEN: Shared LiteLLM proxy key for Claude Code sub-agent
39
+ - ANTHROPIC_BASE_URL: LLM API base URL (default: https://api.anthropic.com)
40
+ - CLAUDE_SUB_AGENT_DEFAULT_MODEL: Default model for sub_agent when SUBAGENT_CLI=claude (default: sonnet)
41
+ - OPENCODE_SUB_AGENT_DEFAULT_MODEL: Default model for sub_agent when SUBAGENT_CLI=opencode (no default — required)
42
+ - CODEX_SUB_AGENT_DEFAULT_MODEL: Default model for sub_agent when SUBAGENT_CLI=codex (no default — required)
43
+ - SUB_AGENT_MAX_TURNS: Default max turns for sub_agent (default: 25)
44
+ - SUB_AGENT_TIMEOUT: Timeout for sub_agent execution in seconds (default: 3600)
45
+
46
+ LiteLLM Integration:
47
+ mcp_servers:
48
+ docker_ai:
49
+ url: "http://computer-use-server:8081/mcp"
50
+ transport: "http"
51
+ auth_type: "bearer_token"
52
+ auth_value: "<MCP_API_KEY>"
53
+ extra_headers:
54
+ # OpenWebUI headers (alternative)
55
+ - "x-openwebui-chat-id"
56
+ - "x-openwebui-user-email"
57
+ - "x-openwebui-user-name"
58
+ - "x-openwebui-gitlab-token"
59
+ - "x-openwebui-gitlab-host"
60
+ - "x-openwebui-anthropic-api-key"
61
+ - "x-openwebui-anthropic-base-url"
62
+ # Direct headers
63
+ - "x-chat-id"
64
+ - "x-user-email"
65
+ - "x-user-name"
66
+ - "x-gitlab-token"
67
+ - "x-gitlab-host"
68
+ - "x-anthropic-api-key"
69
+ - "x-anthropic-base-url"
70
+ """
71
+
72
+ import os
73
+ import re
74
+ import json
75
+ import shlex
76
+ import time
77
+ import asyncio
78
+ import urllib.parse
79
+ from typing import Optional, List, Annotated
80
+
81
+ from mcp.server.fastmcp import FastMCP, Context
82
+ from pydantic import Field
83
+ import skill_manager
84
+ from context_vars import (
85
+ current_chat_id, current_user_email, current_user_name,
86
+ current_gitlab_token, current_gitlab_host,
87
+ current_anthropic_auth_token, current_anthropic_base_url,
88
+ current_mcp_tokens_url, current_mcp_tokens_api_key, current_mcp_servers,
89
+ current_instructions,
90
+ )
91
+ from system_prompt import render_system_prompt
92
+
93
+
94
+ # Single-user mode: "" (lenient default), "true" (solo), "false" (strict multi-user)
95
+ SINGLE_USER_MODE = os.getenv("SINGLE_USER_MODE", "").lower()
96
+
97
+ # Warning appended to tool responses when using default container in lenient mode
98
+ DEFAULT_CHAT_ID_WARNING = (
99
+ "\n\n---\n"
100
+ "Note: No X-Chat-Id header provided — using shared 'default' container.\n"
101
+ "All sessions without a chat ID share the same container (files, processes, state).\n\n"
102
+ "Options:\n"
103
+ "- Set SINGLE_USER_MODE=true in .env to always use one container (single-user setup)\n"
104
+ "- Set SINGLE_USER_MODE=false to require X-Chat-Id (multi-user setup)\n"
105
+ "- Pass X-Chat-Id header in your MCP client for per-session isolation\n"
106
+ )
107
+
108
+ # Error returned when chat_id is missing in strict multi-user mode
109
+ CHAT_ID_REQUIRED_ERROR = (
110
+ "Error: X-Chat-Id header is required (SINGLE_USER_MODE=false).\n\n"
111
+ "In multi-user mode, every request must include X-Chat-Id for container isolation.\n"
112
+ "Pass -H \"X-Chat-Id: your-unique-id\" or set SINGLE_USER_MODE=true for single-user setup."
113
+ )
114
+
115
+
116
+ def _validate_chat_id() -> tuple[str, str | None]:
117
+ """
118
+ Validate chat_id based on SINGLE_USER_MODE setting.
119
+
120
+ Returns:
121
+ tuple: (chat_id, error_message) - error_message is None if valid
122
+ """
123
+ chat_id = current_chat_id.get()
124
+
125
+ if SINGLE_USER_MODE == "true":
126
+ return "default", None
127
+
128
+ if chat_id == "default":
129
+ if SINGLE_USER_MODE == "false":
130
+ return chat_id, CHAT_ID_REQUIRED_ERROR
131
+ return chat_id, None
132
+
133
+ return chat_id, None
134
+
135
+
136
+ def _get_default_chat_warning() -> str:
137
+ """Return warning suffix if using default chat_id in lenient mode."""
138
+ if SINGLE_USER_MODE in ("true", "false"):
139
+ return ""
140
+ if current_chat_id.get() == "default":
141
+ print("[WARN] No X-Chat-Id header and SINGLE_USER_MODE not set — using shared 'default' container")
142
+ return DEFAULT_CHAT_ID_WARNING
143
+ return ""
144
+
145
+
146
+ # Configuration from environment
147
+
148
+ # Docker management extracted to docker_manager.py
149
+ from docker_manager import (
150
+ get_docker_client, get_container_cdp_address,
151
+ _get_or_create_container, _execute_bash, execute_bash_streaming, _execute_python_with_stdin,
152
+ _reset_shutdown_timer, _get_compose_network_name,
153
+ build_mcp_config, build_mcp_config_write_script,
154
+ _fetch_gitlab_token, _ensure_gitlab_token,
155
+ DOCKER_SOCKET, DOCKER_IMAGE, CONTAINER_MEM_LIMIT, CONTAINER_CPU_LIMIT,
156
+ COMMAND_TIMEOUT, ENABLE_NETWORK, USER_DATA_BASE_PATH, PUBLIC_BASE_URL,
157
+ MCP_TOKENS_URL, MCP_TOKENS_API_KEY,
158
+ SUB_AGENT_MAX_TURNS, SUB_AGENT_TIMEOUT,
159
+ ANTHROPIC_DEFAULT_SONNET_MODEL,
160
+ ANTHROPIC_DEFAULT_OPUS_MODEL,
161
+ ANTHROPIC_DEFAULT_HAIKU_MODEL,
162
+ )
163
+ from cli_runtime import dispatch as cli_dispatch, Cli, resolve_cli, resolve_subagent_model
164
+
165
+
166
+
167
+ # ============================================================================
168
+ # Progress Utilities (moved from computer_use_tools.py for server-side use)
169
+ # ============================================================================
170
+
171
+ def format_elapsed_time(seconds: int) -> str:
172
+ """Format elapsed time as human-readable string (e.g., '45s', '2m 15s')."""
173
+ if seconds < 60:
174
+ return f"{seconds}s"
175
+ minutes = seconds // 60
176
+ remaining_seconds = seconds % 60
177
+ if remaining_seconds == 0:
178
+ return f"{minutes}m"
179
+ return f"{minutes}m {remaining_seconds}s"
180
+
181
+
182
+ _TOOL_LABELS = {
183
+ "Bash": "Command",
184
+ "Read": "Reading",
185
+ "Write": "Writing",
186
+ "Edit": "Editing",
187
+ "Grep": "Searching",
188
+ "Glob": "Finding files",
189
+ "WebSearch": "Web search",
190
+ "WebFetch": "Loading page",
191
+ "TodoWrite": "Tasks",
192
+ "Agent": "Subtask",
193
+ "ToolSearch": "Selecting tool",
194
+ }
195
+
196
+
197
+ def parse_last_action(lines: list) -> Optional[str]:
198
+ """
199
+ Parse JSONL lines from Claude session log and return last meaningful action.
200
+ Returns whichever came last in the log (text or tool_use) to avoid showing
201
+ stale text while a long tool is executing.
202
+ """
203
+ last_action = None
204
+
205
+ for line in lines:
206
+ if not line.strip():
207
+ continue
208
+ try:
209
+ data = json.loads(line)
210
+ if data.get("type") == "assistant":
211
+ content = data.get("message", {}).get("content", [])
212
+ for item in content:
213
+ if item.get("type") == "text":
214
+ text = item.get("text", "")[:80]
215
+ text = text.replace('\n', ' ').strip()
216
+ if text:
217
+ last_action = text
218
+ elif item.get("type") == "tool_use":
219
+ name = item.get("name", "unknown")
220
+ inp = item.get("input", {})
221
+ detail = get_tool_detail(name, inp)
222
+ tool_label = _TOOL_LABELS.get(name, name)
223
+ if detail:
224
+ last_action = f"{tool_label}: {detail}"
225
+ else:
226
+ last_action = f"{tool_label}..."
227
+ except (json.JSONDecodeError, KeyError, TypeError):
228
+ continue
229
+
230
+ return last_action
231
+
232
+
233
+ def get_tool_detail(name: str, inp: dict) -> Optional[str]:
234
+ """Extract useful detail from tool input for status display."""
235
+ try:
236
+ # First check for description field (Claude fills this for our tools)
237
+ desc = inp.get("description", "")
238
+ if desc:
239
+ return desc.replace('\n', ' ').strip()
240
+
241
+ # Fallback to tool-specific extraction
242
+ if name == "Bash":
243
+ cmd = inp.get("command", "")
244
+ return cmd.replace('\n', ' ').strip() if cmd else None
245
+ elif name == "WebSearch":
246
+ return inp.get("query", "")
247
+ elif name == "Write":
248
+ path = inp.get("file_path", "")
249
+ return path.split("/")[-1] if path else None
250
+ elif name == "Read":
251
+ path = inp.get("file_path", "")
252
+ return path.split("/")[-1] if path else None
253
+ elif name == "Edit":
254
+ path = inp.get("file_path", "")
255
+ return path.split("/")[-1] if path else None
256
+ elif name == "Grep":
257
+ pattern = inp.get("pattern", "")
258
+ return f'"{pattern}"' if pattern else None
259
+ elif name == "Glob":
260
+ return inp.get("pattern", "")
261
+ elif name == "TodoWrite":
262
+ todos = inp.get("todos", [])
263
+ in_progress = [t for t in todos if t.get("status") == "in_progress"]
264
+ if in_progress:
265
+ return in_progress[0].get("content", "")
266
+ return f"{len(todos)} tasks"
267
+ except Exception:
268
+ pass
269
+ return None
270
+
271
+
272
+
273
+ # ============================================================================
274
+ # MCP Server Definition
275
+ # ============================================================================
276
+
277
+ # Static instructions kwarg — fallback when Tier 4's dynamic override is
278
+ # bypassed (client that ignores InitializeResult.instructions, or the
279
+ # render_system_prompt pre-render failed). Points at the other tiers so any
280
+ # client hitting this baseline learns where to fetch the real content.
281
+ _STATIC_INSTRUCTIONS = (
282
+ "Computer Use tools: bash, file edits, browser, sub-agent — in an isolated "
283
+ "Docker sandbox. Full per-session guide is at /home/assistant/README.md "
284
+ "(call the view tool to read it). Uploaded files are exposed via "
285
+ "resources/list."
286
+ )
287
+
288
+ mcp = FastMCP(
289
+ name="computer-use-mcp",
290
+ instructions=_STATIC_INSTRUCTIONS,
291
+ streamable_http_path="/", # Root path — mounted at /mcp in FastAPI
292
+ stateless_http=True, # Each request is independent (no session persistence)
293
+ transport_security={ # Behind proxy (LiteLLM/nginx), any Host is valid
294
+ "enable_dns_rebinding_protection": False,
295
+ },
296
+ )
297
+
298
+
299
+ # ============================================================================
300
+ # Tier 4 — Dynamic InitializeResult.instructions
301
+ # ============================================================================
302
+ #
303
+ # The static `instructions=` kwarg above ships as a constant in every
304
+ # InitializeResult. We want per-chat content (file URLs, skills) to ride in
305
+ # that same field so clients like Claude Desktop / MCP Inspector (which
306
+ # render `instructions` directly) get dynamic content without any explicit
307
+ # prompts/get call.
308
+ #
309
+ # Mechanism (works ONLY because stateless_http=True):
310
+ # 1. Middleware awaits render_system_prompt(chat_id, user_email) BEFORE
311
+ # dispatching the MCP handler and stores the result in
312
+ # `current_instructions` ContextVar (see MCPContextMiddleware below).
313
+ # 2. `streamable_http_manager._handle_stateless_request` (verified at
314
+ # .venv/.../mcp/server/streamable_http_manager.py:196) spins up a fresh
315
+ # `server.run(..., initialization_options, stateless=True)` per HTTP
316
+ # request, and `create_initialization_options()` is called INSIDE that
317
+ # per-request task — after the middleware has run.
318
+ # 3. `lowlevel/server.py:188` reads `self.instructions` at that moment.
319
+ # We override the property to return the ContextVar value.
320
+ # 4. `session.py:183` echoes it into `InitializeResult.instructions`.
321
+ #
322
+ # Stateful mode would break this: a long-lived session caches init_options at
323
+ # construction time. Do NOT flip stateless_http=False without re-reading the
324
+ # SDK source above.
325
+ #
326
+ # Private-API caveat: we swap `mcp._mcp_server.__class__` in place. FastMCP
327
+ # doesn't expose a public hook. Pin the `mcp` version in requirements.txt to
328
+ # protect against attribute renames.
329
+ from mcp.server.lowlevel.server import Server as _LowlevelServer
330
+
331
+
332
+ class _DynamicInstructionsServer(_LowlevelServer):
333
+ """Subclass that reads `instructions` from the current-request ContextVar.
334
+
335
+ Falls back to the static string if the middleware hasn't pre-rendered
336
+ (e.g. a render exception, or a direct in-process call without ASGI)."""
337
+
338
+ @property
339
+ def instructions(self): # type: ignore[override]
340
+ return current_instructions.get() or self._static_instructions
341
+
342
+ @instructions.setter
343
+ def instructions(self, value):
344
+ self._static_instructions = value
345
+
346
+
347
+ # Rebind class on the already-constructed lowlevel Server so the property
348
+ # override takes effect without reconstructing FastMCP. The base class stores
349
+ # `instructions` in `self.__dict__`; move it to the `_static_instructions`
350
+ # slot before swapping the class so the property getter can read it as the
351
+ # fallback.
352
+ #
353
+ # Defensive shape assertions — these guard against silent breakage when the
354
+ # `mcp` SDK changes the private attribute layout (e.g. moves `_mcp_server` to
355
+ # `_lowlevel_server`, or switches to __slots__). Without them, an SDK rename
356
+ # would silently drop us back to static instructions for every chat — Tier 4
357
+ # would just stop working with no error to debug.
358
+ assert hasattr(mcp, "_mcp_server"), (
359
+ "FastMCP no longer exposes _mcp_server — Tier 4 dynamic instructions "
360
+ "broke. Re-pin mcp in requirements.txt and update mcp_tools.py."
361
+ )
362
+ _existing_lowlevel_server = mcp._mcp_server # private; pinned mcp version guards
363
+ assert isinstance(_existing_lowlevel_server, _LowlevelServer), (
364
+ f"mcp._mcp_server is not a lowlevel Server (got {type(_existing_lowlevel_server)!r}). "
365
+ "Tier 4 class-swap will not work. Re-pin mcp."
366
+ )
367
+ assert hasattr(_existing_lowlevel_server, "__dict__"), (
368
+ "Lowlevel Server uses __slots__ — class-swap pop() will fail. Re-pin mcp."
369
+ )
370
+ _existing_instructions_value = _existing_lowlevel_server.__dict__.pop(
371
+ "instructions", _STATIC_INSTRUCTIONS
372
+ )
373
+ _existing_lowlevel_server._static_instructions = _existing_instructions_value
374
+ _existing_lowlevel_server.__class__ = _DynamicInstructionsServer
375
+
376
+
377
+ async def send_progress(ctx: "Context", progress: float, total: float, message: str):
378
+ """Send progress notification with related_request_id for stateless HTTP mode.
379
+
380
+ Workaround for MCP SDK bug: ctx.report_progress() doesn't pass
381
+ related_request_id, so notifications get lost in stateless_http mode
382
+ (routed to non-existent GET SSE stream instead of request stream).
383
+ """
384
+ rc = ctx.request_context
385
+ if not rc or not rc.meta or not rc.meta.progressToken:
386
+ return
387
+ await rc.session.send_progress_notification(
388
+ progress_token=rc.meta.progressToken,
389
+ progress=progress,
390
+ total=total,
391
+ message=message,
392
+ related_request_id=str(rc.request_id),
393
+ )
394
+
395
+
396
+ # Custom type for view_range
397
+ ViewRange = Annotated[
398
+ Optional[List[int]],
399
+ Field(
400
+ default=None,
401
+ min_length=2,
402
+ max_length=2,
403
+ description="Optional line range [start_line, end_line]. Use [start, -1] to view from start to end."
404
+ )
405
+ ]
406
+
407
+
408
+ # ---------------------------------------------------------------------------
409
+ # Output truncation & command semantics (inspired by Claude Code BashTool)
410
+ # ---------------------------------------------------------------------------
411
+
412
+ MAX_BASH_OUTPUT_CHARS = 30_000
413
+
414
+ # Commands where exit code 1 is NOT an error (semantic exit codes)
415
+ # grep/rg: 1=no matches, 2+=error
416
+ # find: 1=partial access, 2+=error
417
+ # diff: 1=files differ, 2+=error
418
+ # test/[: 1=condition false, 2+=error
419
+ COMMAND_SEMANTICS = {
420
+ 'grep': {'threshold': 2, 'message': 'No matches found'},
421
+ 'rg': {'threshold': 2, 'message': 'No matches found'},
422
+ 'find': {'threshold': 2, 'message': 'Some directories were inaccessible'},
423
+ 'diff': {'threshold': 2, 'message': 'Files differ'},
424
+ 'test': {'threshold': 2, 'message': 'Condition is false'},
425
+ '[': {'threshold': 2, 'message': 'Condition is false'},
426
+ }
427
+
428
+
429
+ def _get_first_command(command: str) -> str:
430
+ """Extract the first command name from a shell command string."""
431
+ cmd = command.strip()
432
+ # Skip env vars like VAR=val, sudo, etc.
433
+ for token in cmd.split():
434
+ if '=' in token:
435
+ continue
436
+ if token in ('sudo', 'env', 'nice', 'time', 'strace'):
437
+ continue
438
+ # Return basename (e.g. /usr/bin/grep -> grep)
439
+ return token.rsplit('/', 1)[-1]
440
+ return ''
441
+
442
+
443
+ def _apply_command_semantics(command: str, exit_code: int, output: str) -> str:
444
+ """Apply command-specific exit code interpretation."""
445
+ if exit_code == 0:
446
+ return output if output else "[No output]"
447
+
448
+ first_cmd = _get_first_command(command)
449
+ semantic = COMMAND_SEMANTICS.get(first_cmd)
450
+
451
+ if semantic and exit_code < semantic['threshold']:
452
+ # Exit code is informational, not an error
453
+ return output if output else semantic['message']
454
+
455
+ # Default: return output or exit code
456
+ return output if output else f"[Exit code: {exit_code}]"
457
+
458
+
459
+ def _truncate_output(output: str, max_chars: int = MAX_BASH_OUTPUT_CHARS) -> str:
460
+ """Truncate large output, keeping head and tail."""
461
+ if len(output) <= max_chars:
462
+ return output
463
+ half = max_chars // 2
464
+ total = len(output)
465
+ return (
466
+ output[:half]
467
+ + f"\n\n... [Output truncated: {total} chars total, showing first and last {half} chars.\n"
468
+ + f"Use head/tail/view to read specific parts] ...\n\n"
469
+ + output[-half:]
470
+ )
471
+
472
+
473
+ @mcp.tool()
474
+ async def bash_tool(command: str, description: str, ctx: Context) -> str:
475
+ """
476
+ Run a bash command in the container.
477
+
478
+ If you've lost track of your environment (chat_id, file URLs, available
479
+ skills), re-read /home/assistant/README.md.
480
+
481
+ Args:
482
+ command: Bash command to run in container
483
+ description: Why I'm running this command
484
+
485
+ Returns:
486
+ Command output (stdout/stderr)
487
+ """
488
+ chat_id, error = _validate_chat_id()
489
+ if error:
490
+ return error
491
+
492
+ try:
493
+ await _ensure_gitlab_token()
494
+
495
+ timeout = int(os.getenv("COMMAND_TIMEOUT", "120"))
496
+
497
+ try:
498
+ container = await asyncio.wait_for(
499
+ asyncio.to_thread(_get_or_create_container, chat_id),
500
+ timeout=60,
501
+ )
502
+ except asyncio.TimeoutError:
503
+ return "Error: Container creation timed out (60s). Docker may be overloaded."
504
+
505
+ # Report progress during execution
506
+ start_time = time.time()
507
+ last_output_line: list[str] = [""]
508
+
509
+ def _on_output_line(line: str) -> None:
510
+ last_output_line[0] = line
511
+
512
+ async def _progress_heartbeat():
513
+ while True:
514
+ await asyncio.sleep(15)
515
+ elapsed = int(time.time() - start_time)
516
+ msg = f"Running: {description} ({format_elapsed_time(elapsed)})"
517
+ last = last_output_line[0]
518
+ if last:
519
+ msg += f"\n→ {last}"
520
+ await send_progress(ctx, elapsed, timeout, msg)
521
+
522
+ heartbeat = asyncio.create_task(_progress_heartbeat())
523
+ try:
524
+ result = await asyncio.to_thread(
525
+ execute_bash_streaming, container, command, timeout, _on_output_line
526
+ )
527
+ finally:
528
+ heartbeat.cancel()
529
+ try:
530
+ await heartbeat
531
+ except asyncio.CancelledError:
532
+ pass
533
+
534
+ output = _apply_command_semantics(command, result["exit_code"], result["output"])
535
+ return _truncate_output(output) + _get_default_chat_warning()
536
+ except Exception as e:
537
+ return f"Error: {str(e)}"
538
+
539
+
540
+ @mcp.tool()
541
+ async def str_replace(
542
+ description: str,
543
+ old_str: str,
544
+ path: str,
545
+ new_str: str = "",
546
+ ctx: Context = None, # injected by FastMCP; None when called directly
547
+ ) -> str:
548
+ """
549
+ Replace a unique string in a file with another string.
550
+ The string to replace must appear exactly once in the file.
551
+
552
+ Args:
553
+ description: Why I'm making this edit
554
+ old_str: String to replace (must be unique in file)
555
+ path: Path to the file to edit
556
+ new_str: String to replace with (empty to delete)
557
+
558
+ Returns:
559
+ Success message or error
560
+ """
561
+ chat_id, error = _validate_chat_id()
562
+ if error:
563
+ return error
564
+
565
+ if old_str == new_str:
566
+ return "Error: old_str and new_str are identical. No changes would be made."
567
+
568
+ try:
569
+ await _ensure_gitlab_token()
570
+ try:
571
+ container = await asyncio.wait_for(
572
+ asyncio.to_thread(_get_or_create_container, chat_id), timeout=60
573
+ )
574
+ except asyncio.TimeoutError:
575
+ return "Error: Container creation timed out (60s)."
576
+
577
+ script = """
578
+ import sys
579
+ import json
580
+
581
+ try:
582
+ data = json.loads(sys.stdin.read())
583
+ path = data['path']
584
+ old_str = data['old_str']
585
+ new_str = data['new_str']
586
+
587
+ with open(path, 'r') as f:
588
+ content = f.read()
589
+
590
+ if old_str not in content:
591
+ print(f"Error: old_str not found in {path}")
592
+ sys.exit(1)
593
+
594
+ count = content.count(old_str)
595
+ if count > 1:
596
+ print(f"Error: Found {count} occurrences of old_str in {path}. Add more surrounding context to make it unique.")
597
+ sys.exit(1)
598
+
599
+ new_content = content.replace(old_str, new_str, 1)
600
+
601
+ with open(path, 'w') as f:
602
+ f.write(new_content)
603
+
604
+ print(f"Successfully replaced text in {path}")
605
+ except Exception as e:
606
+ print(f"Error: {e}")
607
+ sys.exit(1)
608
+ """
609
+ payload = json.dumps({"path": path, "old_str": old_str, "new_str": new_str})
610
+ result = await asyncio.to_thread(_execute_python_with_stdin, container, script, payload)
611
+ return result["output"] + _get_default_chat_warning()
612
+
613
+ except Exception as e:
614
+ return f"Error: {str(e)}"
615
+
616
+
617
+ @mcp.tool()
618
+ async def create_file(
619
+ description: str,
620
+ file_text: str,
621
+ path: str,
622
+ ctx: Context = None,
623
+ ) -> str:
624
+ """
625
+ Create a new file with content in the container.
626
+
627
+ Args:
628
+ description: Why I'm creating this file. ALWAYS PROVIDE THIS PARAMETER FIRST.
629
+ file_text: Content to write to the file. ALWAYS PROVIDE THIS PARAMETER SECOND.
630
+ path: Path to the file to create. ALWAYS PROVIDE THIS PARAMETER LAST.
631
+
632
+ Returns:
633
+ Success message or error
634
+ """
635
+ chat_id, error = _validate_chat_id()
636
+ if error:
637
+ return error
638
+
639
+ try:
640
+ await _ensure_gitlab_token()
641
+ try:
642
+ container = await asyncio.wait_for(
643
+ asyncio.to_thread(_get_or_create_container, chat_id), timeout=60
644
+ )
645
+ except asyncio.TimeoutError:
646
+ return "Error: Container creation timed out (60s)."
647
+
648
+ script = """
649
+ import sys
650
+ import json
651
+ import os
652
+
653
+ try:
654
+ data = json.loads(sys.stdin.read())
655
+ path = data['path']
656
+ file_text = data['file_text']
657
+
658
+ os.makedirs(os.path.dirname(path), exist_ok=True)
659
+
660
+ with open(path, 'w') as f:
661
+ f.write(file_text)
662
+
663
+ print(f"Successfully created {path}")
664
+ except Exception as e:
665
+ print(f"Error: {e}")
666
+ sys.exit(1)
667
+ """
668
+ payload = json.dumps({"path": path, "file_text": file_text})
669
+ result = await asyncio.to_thread(_execute_python_with_stdin, container, script, payload)
670
+ output = result["output"] if result["success"] else f"Error: {result['output']}"
671
+ return output + _get_default_chat_warning()
672
+
673
+ except Exception as e:
674
+ return f"Error: {str(e)}"
675
+
676
+
677
+ @mcp.tool()
678
+ async def view(
679
+ description: str,
680
+ path: str,
681
+ view_range: Optional[List[int]] = None,
682
+ ctx: Context = None,
683
+ ) -> str:
684
+ """
685
+ View text files or directory listings.
686
+ Binary files are detected and rejected with instructions to read SKILL documentation.
687
+
688
+ If you've lost track of your environment (chat_id, file URLs, available
689
+ skills), re-read /home/assistant/README.md.
690
+
691
+ Supported path types:
692
+ - Directories: Lists files and directories with details
693
+ - Text files: Displays numbered lines. You can optionally specify a view_range.
694
+ - Binary files (.xlsx, .docx, .pptx, .pdf, etc.): Returns error with SKILL.md instructions
695
+
696
+ Args:
697
+ description: Why I need to view this
698
+ path: Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`
699
+ view_range: Optional line range [start_line, end_line]. Use [start, -1] to view from start to end.
700
+
701
+ Returns:
702
+ File contents, directory listing, or error message
703
+ """
704
+ chat_id, error = _validate_chat_id()
705
+ if error:
706
+ return error
707
+
708
+ try:
709
+ await _ensure_gitlab_token()
710
+ try:
711
+ container = await asyncio.wait_for(
712
+ asyncio.to_thread(_get_or_create_container, chat_id), timeout=60
713
+ )
714
+ except asyncio.TimeoutError:
715
+ return "Error: Container creation timed out (60s)."
716
+
717
+ quoted_path = shlex.quote(path)
718
+
719
+ # Image extensions — handled separately (resize+return as image content)
720
+ image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.webp'}
721
+
722
+ # Binary file hints (non-image)
723
+ binary_file_hints = {
724
+ '.xlsx': 'Excel spreadsheet. Read SKILL first:\n view /mnt/skills/public/xlsx/SKILL.md',
725
+ '.xls': 'Excel spreadsheet (old). Read SKILL first:\n view /mnt/skills/public/xlsx/SKILL.md',
726
+ '.docx': 'Word document. Read SKILL first:\n view /mnt/skills/public/docx/SKILL.md',
727
+ '.pptx': 'PowerPoint. Read SKILL first:\n view /mnt/skills/public/pptx/SKILL.md',
728
+ '.pdf': 'PDF document. Read SKILL first:\n view /mnt/skills/public/pdf/SKILL.md',
729
+ '.zip': 'ZIP archive. Use: unzip -l {path}',
730
+ '.tar': 'TAR archive. Use: tar -tvf {path}',
731
+ '.gz': 'Gzip file. Use: gunzip -c {path} | head -n 100',
732
+ }
733
+
734
+ file_ext = None
735
+ path_lower = path.lower()
736
+ for ext in list(binary_file_hints.keys()) + list(image_extensions):
737
+ if path_lower.endswith(ext):
738
+ file_ext = ext
739
+ break
740
+
741
+ if file_ext and file_ext in image_extensions:
742
+ # Image file — resize+compress in container, return as structured content
743
+ try:
744
+ py_code = (
745
+ "from PIL import Image; from io import BytesIO; import base64,sys; "
746
+ f"img=Image.open({path!r}); "
747
+ "mx=1280; "
748
+ "img.thumbnail((mx,mx),Image.Resampling.LANCZOS) if max(img.size)>mx else None; "
749
+ "img=img.convert('RGB') if img.mode in ('RGBA','P') else img; "
750
+ "b=BytesIO(); img.save(b,format='JPEG',quality=80); "
751
+ "sys.stdout.write(base64.b64encode(b.getvalue()).decode())"
752
+ )
753
+ resize_cmd = f"python3 -c {shlex.quote(py_code)}"
754
+ b64_result = await asyncio.to_thread(_execute_bash, container, resize_cmd)
755
+ if b64_result["exit_code"] != 0:
756
+ return f"Error viewing image {path}: {b64_result['output']}"
757
+ image_b64 = b64_result["output"].strip()
758
+ return [
759
+ {"type": "text", "text": f"Image: {path}"},
760
+ {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_b64}"}}
761
+ ]
762
+ except Exception as e:
763
+ return f"Error processing image {path}: {e}"
764
+
765
+ elif file_ext and file_ext in binary_file_hints:
766
+ hint = binary_file_hints[file_ext].format(path=path)
767
+ command = f"""
768
+ if [ -f {quoted_path} ]; then
769
+ echo "Error: Cannot view binary file with 'cat'. This is a {file_ext} file."
770
+ echo ""
771
+ echo "{hint}"
772
+ exit 1
773
+ elif [ -d {quoted_path} ]; then
774
+ ls -lah {quoted_path}
775
+ else
776
+ echo "Error: path not found"
777
+ exit 1
778
+ fi
779
+ """
780
+ else:
781
+ if view_range:
782
+ start, end = view_range
783
+ if end == -1:
784
+ cat_command = f"sed -n '{start},$p' {quoted_path} | cat -n"
785
+ else:
786
+ cat_command = f"sed -n '{start},{end}p' {quoted_path} | cat -n"
787
+ else:
788
+ cat_command = f"cat -n {quoted_path}"
789
+
790
+ command = f"""
791
+ if [ -f {quoted_path} ]; then
792
+ {cat_command}
793
+ elif [ -d {quoted_path} ]; then
794
+ ls -lah {quoted_path}
795
+ else
796
+ echo "Error: path not found"
797
+ exit 1
798
+ fi
799
+ """
800
+
801
+ result = await asyncio.to_thread(_execute_bash, container, command)
802
+ output = result["output"] if result["output"] else "Error: No output"
803
+
804
+ # Truncate if needed (30K limit, matching bash_tool MAX_BASH_OUTPUT_CHARS)
805
+ if not view_range and len(output) > 30000:
806
+ truncation_msg = f"\n\n... [File truncated - middle omitted. Total: {len(output)} chars. Use view_range.] ...\n\n"
807
+ output = output[:15000] + truncation_msg + output[-15000:]
808
+
809
+ return output + _get_default_chat_warning()
810
+
811
+ except Exception as e:
812
+ return f"Error: {str(e)}"
813
+
814
+
815
+ # ---------------------------------------------------------------------------
816
+ # Per-CLI docstring variants for the sub_agent tool.
817
+ # FastMCP captures fn.__doc__ at mcp.add_tool() registration time, so we
818
+ # assign the correct variant before calling mcp.add_tool(sub_agent).
819
+ # See RESEARCH.md "FastMCP Docstring Binding Mechanics" (base.py:66).
820
+ # ---------------------------------------------------------------------------
821
+
822
+ _SUBAGENT_DOC_CLAUDE = """Spawn a sub-agent (claude code) to perform a focused subtask.
823
+
824
+ COSTLY: Spawns a separate Claude CLI session with its own API budget.
825
+ Use ONLY as a last resort for complex CODE tasks requiring 10+ iterative tool calls.
826
+
827
+ Justified uses:
828
+ - Multi-file refactoring (5+ files) with test verification loops
829
+ - Complex code review with automatic fixes across many files
830
+ - Iterative test-fix cycles (run tests, analyze, fix, re-run until pass)
831
+
832
+ Do NOT use for (handle these yourself):
833
+ - Tasks completable in fewer than 10 tool calls
834
+ - Creating presentations, documents, spreadsheets
835
+ - Web research or information gathering
836
+ - Simple code review, documentation, or analysis
837
+ - Git operations or simple file edits
838
+
839
+ Args:
840
+ task: Detailed description of the task for the sub-agent to accomplish
841
+ description: Why you are delegating this task to a sub-agent
842
+ model: Claude model alias or id. Aliases: 'sonnet' (default, fast), 'opus' (powerful,
843
+ slower), 'haiku' (cheapest). Pass an empty string to use the per-CLI default
844
+ (claude -> sonnet). Operator override: CLAUDE_SUB_AGENT_DEFAULT_MODEL env.
845
+ max_turns: Maximum number of agentic turns (default from env, typically 25)
846
+ working_directory: Working directory for the agent (default: /home/assistant)
847
+ resume_session_id: Session ID to resume a previous sub-agent session (from previous result)
848
+
849
+ Returns:
850
+ Sub-agent's response with task results, cost, turn count, and session_id for resume
851
+
852
+ Tip: Run `list-subagent-models` (or `bash /mnt/skills/public/sub-agent/scripts/list_subagent_models.sh`)
853
+ to discover valid model ids for the active SUBAGENT_CLI.
854
+ """
855
+
856
+ _SUBAGENT_DOC_OPENCODE = """Spawn a sub-agent (opencode) to perform a focused subtask.
857
+
858
+ COSTLY: Spawns a separate opencode CLI session with its own API budget.
859
+ Use ONLY as a last resort for complex CODE tasks requiring 10+ iterative tool calls.
860
+
861
+ Justified uses:
862
+ - Multi-file refactoring (5+ files) with test verification loops
863
+ - Complex code review with automatic fixes across many files
864
+ - Iterative test-fix cycles (run tests, analyze, fix, re-run until pass)
865
+
866
+ Do NOT use for (handle these yourself):
867
+ - Tasks completable in fewer than 10 tool calls
868
+ - Creating presentations, documents, spreadsheets
869
+ - Web research or information gathering
870
+ - Simple code review, documentation, or analysis
871
+ - Git operations or simple file edits
872
+
873
+ Args:
874
+ task: Detailed description of the task for the sub-agent to accomplish
875
+ description: Why you are delegating this task to a sub-agent
876
+ model: opencode model id in `provider/model` form (e.g. `anthropic/claude-sonnet-4-6`,
877
+ `openrouter/qwen/qwen-3-coder`). Pass an empty string to use the per-CLI default
878
+ (controlled by OPENCODE_SUB_AGENT_DEFAULT_MODEL env). Operators can extend the
879
+ alias vocabulary via the OPENCODE_MODEL_ALIASES env var (JSON object string).
880
+ max_turns: Maximum number of agentic turns (default from env, typically 25)
881
+ working_directory: Working directory for the agent (default: /home/assistant)
882
+ resume_session_id: Session ID to resume a previous sub-agent session (from previous result)
883
+
884
+ Returns:
885
+ Sub-agent's response with task results, cost, turn count, and session_id for resume
886
+
887
+ Required: Run `list-subagent-models` (or `bash /mnt/skills/public/sub-agent/scripts/list_subagent_models.sh`)
888
+ to discover valid model ids for opencode before calling this tool. Pass concrete ids; do not use Claude aliases.
889
+ """
890
+
891
+ _SUBAGENT_DOC_CODEX = """Spawn a sub-agent (codex) to perform a focused subtask.
892
+
893
+ COSTLY: Spawns a separate codex CLI session with its own API budget.
894
+ Use ONLY as a last resort for complex CODE tasks requiring 10+ iterative tool calls.
895
+
896
+ Justified uses:
897
+ - Multi-file refactoring (5+ files) with test verification loops
898
+ - Complex code review with automatic fixes across many files
899
+ - Iterative test-fix cycles (run tests, analyze, fix, re-run until pass)
900
+
901
+ Do NOT use for (handle these yourself):
902
+ - Tasks completable in fewer than 10 tool calls
903
+ - Creating presentations, documents, spreadsheets
904
+ - Web research or information gathering
905
+ - Simple code review, documentation, or analysis
906
+ - Git operations or simple file edits
907
+
908
+ Args:
909
+ task: Detailed description of the task for the sub-agent to accomplish
910
+ description: Why you are delegating this task to a sub-agent
911
+ model: codex model id (fully qualified — codex requires concrete ids, no aliases). Pass an
912
+ empty string to use the per-CLI default (controlled by CODEX_SUB_AGENT_DEFAULT_MODEL
913
+ env). If neither a caller model nor the env var is set, the tool returns an error
914
+ pointing to list-subagent-models for discovery.
915
+ max_turns: Maximum number of agentic turns (default from env, typically 25)
916
+ working_directory: Working directory for the agent (default: /home/assistant)
917
+ resume_session_id: Session ID to resume a previous sub-agent session (from previous result)
918
+
919
+ Returns:
920
+ Sub-agent's response with task results, cost, turn count, and session_id for resume
921
+
922
+ Required: Run `list-subagent-models` (or `bash /mnt/skills/public/sub-agent/scripts/list_subagent_models.sh`)
923
+ to discover valid model ids for codex before calling this tool.
924
+ """
925
+
926
+
927
+ def _subagent_docstring_for_cli(cli: str) -> str:
928
+ """Return the FastMCP Tool.description for sub_agent given the active CLI.
929
+
930
+ Falls back to the claude variant for unknown CLI values (mirrors resolve_cli's default).
931
+ """
932
+ if cli == "opencode":
933
+ return _SUBAGENT_DOC_OPENCODE
934
+ if cli == "codex":
935
+ return _SUBAGENT_DOC_CODEX
936
+ return _SUBAGENT_DOC_CLAUDE
937
+
938
+
939
+ async def sub_agent(
940
+ task: str,
941
+ description: str,
942
+ ctx: Context,
943
+ model: str = "",
944
+ max_turns: int = 0,
945
+ working_directory: str = "/home/assistant",
946
+ resume_session_id: str = ""
947
+ ) -> str:
948
+ chat_id, error = _validate_chat_id()
949
+ if error:
950
+ return error
951
+
952
+ user_email = current_user_email.get()
953
+
954
+ if max_turns <= 0:
955
+ max_turns = SUB_AGENT_MAX_TURNS
956
+ # Resolve the active CLI runtime — single source of truth.
957
+ cli = resolve_cli()
958
+
959
+ try:
960
+ # Resolve default model inside try so ValueError surfaces as "Sub-agent error: ..."
961
+ # Preserve the resolver's display_name (e.g. "sonnet") so the completion
962
+ # banner shows the friendly alias instead of the full id (e.g.
963
+ # "claude-sonnet-4-6"). dispatch() re-resolves and would only return the
964
+ # id-as-display once the alias has been expanded, losing the friendly name.
965
+ default_display_name = None
966
+ if not model:
967
+ model, default_display_name = resolve_subagent_model("", cli)
968
+ await _ensure_gitlab_token()
969
+ container = await asyncio.to_thread(_get_or_create_container, chat_id)
970
+
971
+ # Write ~/.mcp.json if MCP server names provided via header.
972
+ mcp_servers_str = current_mcp_servers.get()
973
+ if mcp_servers_str:
974
+ mcp_cfg = build_mcp_config(
975
+ mcp_servers_str,
976
+ current_anthropic_base_url.get(),
977
+ user_email or "",
978
+ )
979
+ if mcp_cfg:
980
+ write_cmd = build_mcp_config_write_script(mcp_cfg)
981
+ await asyncio.to_thread(_execute_bash, container, write_cmd, 15)
982
+
983
+ # Build the sub-agent system prompt with dynamic skills.
984
+ file_base_url = f"{PUBLIC_BASE_URL}/files/{chat_id}"
985
+ plan_file = "/home/assistant/task_plan.md"
986
+ skills = (
987
+ skill_manager.get_user_skills_sync(user_email)
988
+ if user_email
989
+ else skill_manager.get_user_skills_sync(None)
990
+ )
991
+ skills_text = skill_manager.build_sub_agent_skills_text(skills)
992
+
993
+ # ANTHROPIC_CUSTOM_HEADERS for LiteLLM user tagging (claude path only;
994
+ # codex/opencode ignore it harmlessly because they don't read it).
995
+ headers_env = ""
996
+ if user_email:
997
+ headers_env = (
998
+ f"ANTHROPIC_CUSTOM_HEADERS="
999
+ f"{shlex.quote(f'x-openwebui-user-email: {user_email}')} "
1000
+ )
1001
+
1002
+ if resume_session_id and cli == Cli.CLAUDE:
1003
+ # Resume path: short prompt, system_prompt empty (claude --resume
1004
+ # restores it from the session).
1005
+ #
1006
+ # Codex/opencode adapters do NOT support session resume — they
1007
+ # emit a stderr warning and start fresh. If we collapsed the
1008
+ # prompt + cleared system_prompt for them too, the "fresh" run
1009
+ # would launch with the generic "Continue working..." stub and
1010
+ # no system context, silently losing the user's task. So gate
1011
+ # the rewrite on claude — codex/opencode keep the original
1012
+ # task + system_prompt and start a fresh, fully-briefed run.
1013
+ task_for_dispatch = (
1014
+ f"Continue working on the task. If needed, re-read "
1015
+ f"{plan_file} for full context."
1016
+ )
1017
+ system_prompt = ""
1018
+ else:
1019
+ # New session: write the task plan file (survives context compaction)
1020
+ # and build the full system prompt with skills.
1021
+ #
1022
+ # SECURITY: encode the task body via base64 + pipe through `base64 -d`
1023
+ # rather than embedding it directly into a shell heredoc. The previous
1024
+ # `cat > {plan_file} << 'TASK_PLAN_EOF'\n{task}\nTASK_PLAN_EOF` pattern
1025
+ # would close early if the task body contained a line equal to the
1026
+ # sentinel `TASK_PLAN_EOF`, causing the remainder to execute as shell
1027
+ # (and corrupting the saved plan even without malice). Base64 is
1028
+ # immune to all shell metacharacters in the payload.
1029
+ # Per CodeRabbit PR#75 review.
1030
+ import base64
1031
+ encoded_task = base64.b64encode(task.encode("utf-8")).decode("ascii")
1032
+ write_plan_cmd = (
1033
+ f"echo {shlex.quote(encoded_task)} | base64 -d > {shlex.quote(plan_file)}"
1034
+ )
1035
+ await asyncio.to_thread(_execute_bash, container, write_plan_cmd, 30)
1036
+
1037
+ system_prompt = f"""<critical_instruction>
1038
+ Your task plan is saved at {plan_file}
1039
+
1040
+ BEFORE ANY ACTION:
1041
+ 1. Read {plan_file} to understand your full task
1042
+ 2. If context becomes compacted, re-read {plan_file} - it is your source of truth
1043
+ 3. The plan file contains all details you need
1044
+
1045
+ Never forget: {plan_file} has your complete instructions.
1046
+ </critical_instruction>
1047
+
1048
+ <environment>
1049
+ You are working in a Linux container (Ubuntu 24) as an autonomous sub-agent.
1050
+ FILE LOCATIONS:
1051
+ - User uploads: /mnt/user-data/uploads (read-only)
1052
+ - Workspace: /home/assistant
1053
+ - Outputs: /mnt/user-data/outputs (URL: {file_base_url}/)
1054
+ </environment>
1055
+
1056
+ <available_skills>
1057
+ IMPORTANT: Read the relevant SKILL.md BEFORE starting any task!
1058
+
1059
+ {skills_text}
1060
+
1061
+ Use `cat <skill-location>` to read skill instructions.
1062
+ </available_skills>"""
1063
+ task_for_dispatch = f"Read and execute your task plan from {plan_file}"
1064
+
1065
+ # Marker file for JSONL session-id discovery — claude-only feature
1066
+ # (codex --ephemeral and opencode run have no equivalent session
1067
+ # JSONL trail; this is gated below in _stream_session_logs).
1068
+ await asyncio.to_thread(
1069
+ _execute_bash, container,
1070
+ "touch /tmp/.sub_agent_start", 5,
1071
+ )
1072
+ start_time = time.time()
1073
+
1074
+ # Stream session logs via tail -f for real-time progress.
1075
+ # Claude-only — codex/opencode get heartbeat-only progress (Phase 7
1076
+ # cost-guardrail-and-ttyd-UX milestone owns the per-CLI progress UX).
1077
+ async def _stream_session_logs():
1078
+ if cli != Cli.CLAUDE:
1079
+ # Heartbeat-only loop for non-claude CLIs.
1080
+ try:
1081
+ while True:
1082
+ await asyncio.sleep(15)
1083
+ elapsed = int(time.time() - start_time)
1084
+ await send_progress(
1085
+ ctx, elapsed, SUB_AGENT_TIMEOUT,
1086
+ f"Agent running... ({format_elapsed_time(elapsed)})",
1087
+ )
1088
+ except asyncio.CancelledError:
1089
+ return
1090
+ except Exception:
1091
+ pass
1092
+ return
1093
+
1094
+ # Claude path — original JSONL log streaming (lifted verbatim
1095
+ # from v0.9.2.0).
1096
+ try:
1097
+ jsonl_path = None
1098
+ for _ in range(60):
1099
+ await asyncio.sleep(1)
1100
+ find_r = await asyncio.to_thread(
1101
+ _execute_bash, container,
1102
+ "find /home/assistant/.claude/projects/-home-assistant/ "
1103
+ "-name '*.jsonl' -newer /tmp/.sub_agent_start "
1104
+ "2>/dev/null | head -1", 5,
1105
+ )
1106
+ path = (find_r.get("output") or "").strip()
1107
+ if path:
1108
+ jsonl_path = path
1109
+ break
1110
+
1111
+ if not jsonl_path:
1112
+ while True:
1113
+ await asyncio.sleep(15)
1114
+ elapsed = int(time.time() - start_time)
1115
+ await send_progress(
1116
+ ctx, elapsed, SUB_AGENT_TIMEOUT,
1117
+ f"Agent running... ({format_elapsed_time(elapsed)})",
1118
+ )
1119
+
1120
+ import threading
1121
+ q = asyncio.Queue()
1122
+ loop = asyncio.get_event_loop()
1123
+ client = get_docker_client()
1124
+
1125
+ def _tail_reader():
1126
+ try:
1127
+ exec_id = client.api.exec_create(
1128
+ container.id, ["tail", "-n", "0", "-f", jsonl_path],
1129
+ stdout=True, stderr=False,
1130
+ )
1131
+ for chunk in client.api.exec_start(exec_id['Id'], stream=True):
1132
+ loop.call_soon_threadsafe(q.put_nowait, chunk)
1133
+ except Exception:
1134
+ pass
1135
+
1136
+ threading.Thread(target=_tail_reader, daemon=True).start()
1137
+
1138
+ buffer = ""
1139
+ while True:
1140
+ try:
1141
+ chunk = await asyncio.wait_for(q.get(), timeout=15)
1142
+ buffer += chunk.decode('utf-8', errors='replace')
1143
+ while '\n' in buffer:
1144
+ line, buffer = buffer.split('\n', 1)
1145
+ action = parse_last_action([line])
1146
+ if action:
1147
+ elapsed = int(time.time() - start_time)
1148
+ msg = f"{action} ({format_elapsed_time(elapsed)})"
1149
+ print(f"[SUB-AGENT-PROGRESS] {msg}")
1150
+ await send_progress(
1151
+ ctx, elapsed, SUB_AGENT_TIMEOUT, msg,
1152
+ )
1153
+ except asyncio.TimeoutError:
1154
+ elapsed = int(time.time() - start_time)
1155
+ await send_progress(
1156
+ ctx, elapsed, SUB_AGENT_TIMEOUT,
1157
+ f"Agent running... ({format_elapsed_time(elapsed)})",
1158
+ )
1159
+ except asyncio.CancelledError:
1160
+ return
1161
+ except Exception:
1162
+ pass
1163
+
1164
+ log_task = asyncio.create_task(_stream_session_logs())
1165
+ try:
1166
+ sub_result, model_id, model_display = await cli_dispatch(
1167
+ container=container,
1168
+ task=task_for_dispatch,
1169
+ system_prompt=system_prompt,
1170
+ model=model,
1171
+ max_turns=max_turns,
1172
+ timeout_s=SUB_AGENT_TIMEOUT,
1173
+ working_directory=working_directory,
1174
+ resume_session_id=resume_session_id,
1175
+ plan_file=plan_file,
1176
+ headers_env=headers_env,
1177
+ )
1178
+ # When caller omitted `model` and we resolved the default above,
1179
+ # restore the friendly display_name (e.g. "sonnet"); dispatch()
1180
+ # would otherwise have surfaced the resolved id ("claude-sonnet-4-6").
1181
+ if default_display_name:
1182
+ model_display = default_display_name
1183
+ finally:
1184
+ log_task.cancel()
1185
+ try:
1186
+ await log_task
1187
+ except asyncio.CancelledError:
1188
+ pass
1189
+
1190
+ duration = time.time() - start_time
1191
+
1192
+ # _find_session_id: claude-only (reads ~/.claude/projects/*.jsonl).
1193
+ async def _find_session_id() -> str:
1194
+ if cli != Cli.CLAUDE:
1195
+ return ""
1196
+ try:
1197
+ find_r = await asyncio.to_thread(
1198
+ _execute_bash, container,
1199
+ "find /home/assistant/.claude/projects/-home-assistant/ "
1200
+ "-name '*.jsonl' -newer /tmp/.sub_agent_start "
1201
+ "2>/dev/null | head -1", 5,
1202
+ )
1203
+ jsonl_path = (find_r.get("output") or "").strip()
1204
+ if jsonl_path:
1205
+ import re
1206
+ m = re.search(r'([0-9a-f-]{36})\.jsonl$', jsonl_path)
1207
+ if m:
1208
+ return m.group(1)
1209
+ except Exception:
1210
+ pass
1211
+ return ""
1212
+
1213
+ # SubAgentResult is the normalised dataclass — render it as the
1214
+ # human-readable string the MCP tool has always returned.
1215
+ # cost_usd is None for codex always, and may be None for opencode —
1216
+ # render "unavailable" rather than "$0.00" (Pitfall 4). Phase 7
1217
+ # cost-guardrail-and-ttyd-UX milestone refines this further.
1218
+ cost_text = (
1219
+ f"${sub_result.cost_usd:.4f}" if sub_result.cost_usd is not None
1220
+ else "unavailable"
1221
+ )
1222
+ turns_text = (
1223
+ f"{sub_result.turns}/{max_turns}" if sub_result.turns is not None
1224
+ else f"?/{max_turns}"
1225
+ )
1226
+ session_id = sub_result.session_id or ""
1227
+
1228
+ # WARNING 1 fix: preserve v0.9.2.0 distinct user-facing messages
1229
+ # for rc=124 (timeout), rc=137 (SIGKILL), rc=143 (SIGTERM), other
1230
+ # non-zero (failed). SubAgentResult.returncode (added in plan
1231
+ # 05-02 Task 0, populated by all three adapters) carries the rc.
1232
+ if sub_result.is_error and not sub_result.text.strip():
1233
+ rc = sub_result.returncode
1234
+ if rc == 124:
1235
+ reason = f"timed out after {SUB_AGENT_TIMEOUT}s"
1236
+ elif rc == 137 or rc == -9:
1237
+ reason = "killed by SIGKILL"
1238
+ elif rc == 143 or rc == -15:
1239
+ reason = "terminated by SIGTERM"
1240
+ elif rc != 0:
1241
+ reason = f"failed with exit code {rc}"
1242
+ else:
1243
+ reason = "crashed before producing results"
1244
+ fallback_session = await _find_session_id()
1245
+ msg = (
1246
+ f"**Sub-Agent Terminated** ({reason})\n"
1247
+ f"**Model:** {model_display} | **Duration:** {duration:.1f}s\n\n"
1248
+ f"Process was killed or crashed before producing results.\n"
1249
+ )
1250
+ if fallback_session:
1251
+ msg += (
1252
+ f"\n**Session ID:** `{fallback_session}` "
1253
+ f"(use resume_session_id to continue)"
1254
+ )
1255
+ return msg
1256
+
1257
+ status = "error" if sub_result.is_error else "success"
1258
+ result_text = (
1259
+ f"**Sub-Agent Completed** ({status})\n"
1260
+ f"**Model:** {model_display} | **Turns:** {turns_text} | "
1261
+ f"**Cost:** {cost_text} | **Duration:** {duration:.1f}s\n\n"
1262
+ f"{sub_result.text}"
1263
+ )
1264
+ if session_id:
1265
+ result_text += (
1266
+ f"\n\n**Session ID:** `{session_id}` "
1267
+ f"(use resume_session_id to continue)"
1268
+ )
1269
+
1270
+ return result_text + _get_default_chat_warning()
1271
+
1272
+ except Exception as e:
1273
+ return f"Sub-agent error: {str(e)}"
1274
+
1275
+
1276
+ # Assign the per-CLI docstring BEFORE registering with FastMCP.
1277
+ # FastMCP captures fn.__doc__ at add_tool() time (base.py:66), so this
1278
+ # must come after the function definition and before mcp.add_tool().
1279
+ sub_agent.__doc__ = _subagent_docstring_for_cli(os.getenv("SUBAGENT_CLI", "claude"))
1280
+ mcp.add_tool(sub_agent)
1281
+
1282
+
1283
+ # ============================================================================
1284
+ # Helper functions for HTTP header integration
1285
+ # ============================================================================
1286
+
1287
+ def set_context_from_headers(headers: dict):
1288
+ """Set context variables from HTTP headers.
1289
+
1290
+ Supports both direct headers (x-chat-id) and OpenWebUI headers (x-openwebui-chat-id).
1291
+ Direct headers take priority over OpenWebUI headers.
1292
+ """
1293
+ # Chat ID (required) - check both formats
1294
+ # Normalize to lowercase: Docker container names are case-sensitive,
1295
+ # and browser URLs may contain uppercase hex in UUIDs
1296
+ if "x-chat-id" in headers:
1297
+ current_chat_id.set(headers["x-chat-id"].lower())
1298
+ elif "x-openwebui-chat-id" in headers:
1299
+ current_chat_id.set(headers["x-openwebui-chat-id"].lower())
1300
+
1301
+ # User email - check both formats
1302
+ if "x-user-email" in headers:
1303
+ current_user_email.set(headers["x-user-email"])
1304
+ elif "x-openwebui-user-email" in headers:
1305
+ current_user_email.set(headers["x-openwebui-user-email"])
1306
+
1307
+ # User name - check both formats (URL-decode: client URL-encodes to handle non-ASCII)
1308
+ if "x-user-name" in headers:
1309
+ current_user_name.set(urllib.parse.unquote(headers["x-user-name"]))
1310
+ elif "x-openwebui-user-name" in headers:
1311
+ current_user_name.set(urllib.parse.unquote(headers["x-openwebui-user-name"]))
1312
+
1313
+ # GitLab token - check both formats
1314
+ if "x-gitlab-token" in headers:
1315
+ current_gitlab_token.set(headers["x-gitlab-token"])
1316
+ elif "x-openwebui-gitlab-token" in headers:
1317
+ current_gitlab_token.set(headers["x-openwebui-gitlab-token"])
1318
+
1319
+ # GitLab host - check both formats
1320
+ if "x-gitlab-host" in headers:
1321
+ current_gitlab_host.set(headers["x-gitlab-host"])
1322
+ elif "x-openwebui-gitlab-host" in headers:
1323
+ current_gitlab_host.set(headers["x-openwebui-gitlab-host"])
1324
+
1325
+ # Anthropic API key - check both formats
1326
+ if "x-anthropic-api-key" in headers:
1327
+ current_anthropic_auth_token.set(headers["x-anthropic-api-key"])
1328
+ elif "x-openwebui-anthropic-api-key" in headers:
1329
+ current_anthropic_auth_token.set(headers["x-openwebui-anthropic-api-key"])
1330
+
1331
+ # Anthropic base URL - check both formats
1332
+ if "x-anthropic-base-url" in headers:
1333
+ current_anthropic_base_url.set(headers["x-anthropic-base-url"])
1334
+ elif "x-openwebui-anthropic-base-url" in headers:
1335
+ current_anthropic_base_url.set(headers["x-openwebui-anthropic-base-url"])
1336
+
1337
+ # MCP Tokens URL - check both formats
1338
+ if "x-mcp-tokens-url" in headers:
1339
+ current_mcp_tokens_url.set(headers["x-mcp-tokens-url"])
1340
+ elif "x-openwebui-mcp-tokens-url" in headers:
1341
+ current_mcp_tokens_url.set(headers["x-openwebui-mcp-tokens-url"])
1342
+
1343
+ # MCP Tokens API key - check both formats
1344
+ if "x-mcp-tokens-api-key" in headers:
1345
+ current_mcp_tokens_api_key.set(headers["x-mcp-tokens-api-key"])
1346
+ elif "x-openwebui-mcp-tokens-api-key" in headers:
1347
+ current_mcp_tokens_api_key.set(headers["x-openwebui-mcp-tokens-api-key"])
1348
+
1349
+ # MCP server names for sub-agent (comma-separated) - check both formats
1350
+ if "x-mcp-servers" in headers:
1351
+ current_mcp_servers.set(headers["x-mcp-servers"])
1352
+ elif "x-openwebui-mcp-servers" in headers:
1353
+ current_mcp_servers.set(headers["x-openwebui-mcp-servers"])
1354
+
1355
+
1356
+ class MCPAuthMiddleware:
1357
+ """ASGI middleware for Bearer token auth on MCP endpoint."""
1358
+
1359
+ def __init__(self, app, api_key: Optional[str] = None):
1360
+ self.app = app
1361
+ self.api_key = api_key
1362
+
1363
+ async def __call__(self, scope, receive, send):
1364
+ if scope["type"] != "http" or not self.api_key:
1365
+ await self.app(scope, receive, send)
1366
+ return
1367
+
1368
+ # Extract Authorization header
1369
+ headers = dict(scope.get("headers", []))
1370
+ auth = headers.get(b"authorization", b"").decode()
1371
+
1372
+ if not auth.startswith("Bearer ") or auth[7:] != self.api_key:
1373
+ # Return 401 Unauthorized
1374
+ response_body = b'{"error": "Unauthorized"}'
1375
+ await send({
1376
+ "type": "http.response.start",
1377
+ "status": 401,
1378
+ "headers": [
1379
+ (b"content-type", b"application/json"),
1380
+ (b"www-authenticate", b"Bearer"),
1381
+ ],
1382
+ })
1383
+ await send({
1384
+ "type": "http.response.body",
1385
+ "body": response_body,
1386
+ })
1387
+ return
1388
+
1389
+ await self.app(scope, receive, send)
1390
+
1391
+
1392
+ class MCPContextMiddleware:
1393
+ """ASGI middleware: HTTP headers → ContextVars before MCP handler.
1394
+
1395
+ Also pre-renders the system prompt and stores it in `current_instructions`
1396
+ so the _DynamicInstructionsServer.instructions property can read it
1397
+ synchronously when building InitializeResult (Tier 4).
1398
+ Rendering is cache-backed (60s TTL per (chat_id, user_email)), so real
1399
+ cost on a hot key is a dict lookup.
1400
+ """
1401
+
1402
+ def __init__(self, app):
1403
+ self.app = app
1404
+
1405
+ async def __call__(self, scope, receive, send):
1406
+ if scope["type"] == "http":
1407
+ headers = {k.decode(): v.decode() for k, v in scope.get("headers", [])}
1408
+ set_context_from_headers(headers)
1409
+
1410
+ # Pre-render system prompt for Tier 4 (dynamic instructions).
1411
+ # Swallow errors — fall back to the static _STATIC_INSTRUCTIONS
1412
+ # string that the _DynamicInstructionsServer getter returns when
1413
+ # current_instructions is None.
1414
+ try:
1415
+ chat_id = current_chat_id.get()
1416
+ user_email = current_user_email.get()
1417
+ rendered = await render_system_prompt(chat_id, user_email)
1418
+ current_instructions.set(rendered)
1419
+ except Exception as e:
1420
+ print(f"[MCP] render_system_prompt warning: {e}")
1421
+ await self.app(scope, receive, send)
1422
+
1423
+
1424
+ def get_mcp_app(api_key: Optional[str] = None):
1425
+ """Get the MCP ASGI app with auth and context middleware for mounting."""
1426
+ app = mcp.streamable_http_app()
1427
+ # Wrap with context middleware (inner) then auth (outer)
1428
+ app = MCPContextMiddleware(app)
1429
+ app = MCPAuthMiddleware(app, api_key=api_key)
1430
+ return app