├── .circleci └── config.yml ├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── .npmrc ├── LICENSE.txt ├── README.md ├── assets ├── alpine_bg.png ├── anthropic_api_payment.png ├── anthropic_signup.png ├── cheerpx.svg ├── discord-mark-blue.svg ├── fork_deploy_instructions.gif ├── github-mark-white.svg ├── insert_key.png ├── leaningtech.png ├── result.png ├── social_2024.png ├── tailscale.svg ├── webvm_claude_ctf.gif ├── webvm_hero.png ├── welcome_to_WebVM_2024.png └── welcome_to_WebVM_alpine_2024.png ├── config_github_terminal.js ├── config_public_alpine.js ├── config_public_terminal.js ├── dockerfiles ├── .dockerignore ├── debian_large └── debian_mini ├── docs └── Tailscale.md ├── documents ├── ArchitectureOverview.png ├── WebAssemblyTools.pdf ├── Welcome.txt └── index.list ├── examples ├── c │ ├── Makefile │ ├── env.c │ ├── helloworld.c │ ├── link.c │ ├── openat.c │ └── waitpid.c ├── lua │ ├── fizzbuzz.lua │ ├── sorting.lua │ └── symmetric_difference.lua ├── nodejs │ ├── environment.js │ ├── nbody.js │ ├── primes.js │ └── wasm.js ├── python3 │ ├── factorial.py │ ├── fibonacci.py │ └── pi.py └── ruby │ ├── helloWorld.rb │ ├── love.rb │ └── powOf2.rb ├── favicon.ico ├── login.html ├── nginx.conf ├── package-lock.json ├── package.json ├── postcss.config.js ├── scrollbar.css ├── serviceWorker.js ├── src ├── app.html ├── lib │ ├── AnthropicTab.svelte │ ├── BlogPost.svelte │ ├── CpuTab.svelte │ ├── DiscordTab.svelte │ ├── DiskTab.svelte │ ├── GitHubTab.svelte │ ├── Icon.svelte │ ├── InformationTab.svelte │ ├── NetworkingTab.svelte │ ├── PanelButton.svelte │ ├── PostsTab.svelte │ ├── SideBar.svelte │ ├── SmallButton.svelte │ ├── WebVM.svelte │ ├── activities.js │ ├── anthropic.js │ ├── global.css │ ├── messages.js │ ├── network.js │ └── plausible.js └── routes │ ├── +layout.server.js │ ├── +page.js │ ├── +page.svelte │ └── alpine │ ├── +page.js │ └── +page.svelte ├── svelte.config.js ├── tailwind.config.js ├── tower.ico ├── vite.config.js └── xterm ├── xterm-addon-fit.js ├── xterm-addon-web-links.js ├── xterm.css └── xterm.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | jobs: 4 | deploy: 5 | docker: 6 | - image: cimg/node:22.9 7 | resource_class: medium 8 | steps: 9 | - add_ssh_keys: 10 | fingerprints: 11 | - "86:3b:c9:a6:d1:b9:a8:dc:0e:00:db:99:8d:19:c4:3e" 12 | - run: 13 | name: Add known hosts 14 | command: | 15 | mkdir -p ~/.ssh 16 | echo $GH_HOST >> ~/.ssh/known_hosts 17 | echo $HAVANA_HOST >> ~/.ssh/known_hosts 18 | - run: 19 | name: Install NPM 20 | command: | 21 | sudo apt-get update && sudo apt-get install -y rsync npm 22 | - run: 23 | name: Clone WebVM 24 | command: | 25 | git clone --branch $CIRCLE_BRANCH --single-branch git@github.com:leaningtech/webvm.git 26 | - run: 27 | name: Build WebVM 28 | command: | 29 | cd webvm/ 30 | npm install 31 | npm run build 32 | - run: 33 | name: Deploy webvm 34 | command: | 35 | rsync -avz --chown circleci:runtimes webvm/build/ circleci@havana.leaningtech.com:webvm/ 36 | 37 | workflows: 38 | deploy: 39 | when: 40 | equal: [ << pipeline.trigger_source >>, "api" ] 41 | jobs: 42 | - deploy 43 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | # Define when the workflow should run 4 | on: 5 | # Allow manual triggering of the workflow from the Actions tab 6 | workflow_dispatch: 7 | 8 | # Allow inputs to be passed when manually triggering the workflow from the Actions tab 9 | inputs: 10 | DOCKERFILE_PATH: 11 | type: string 12 | description: 'Path to the Dockerfile' 13 | required: true 14 | default: 'dockerfiles/debian_mini' 15 | 16 | IMAGE_SIZE: 17 | type: string 18 | description: 'Image size, 950M max' 19 | required: true 20 | default: '600M' 21 | 22 | DEPLOY_TO_GITHUB_PAGES: 23 | type: boolean 24 | description: 'Deploy to Github pages' 25 | required: true 26 | default: true 27 | 28 | GITHUB_RELEASE: 29 | type: boolean 30 | description: 'Upload GitHub release' 31 | required: true 32 | default: false 33 | 34 | jobs: 35 | 36 | guard_clause: 37 | runs-on: ubuntu-latest 38 | 39 | env: 40 | GH_TOKEN: ${{ github.token }} # As required by the GitHub-CLI 41 | 42 | permissions: 43 | actions: 'write' # Required in order to terminate the workflow run. 44 | 45 | steps: 46 | - uses: actions/checkout@v4 47 | # Guard clause that cancels the workflow in case of an invalid DOCKERFILE_PATH and/or incorrectly configured Github Pages. 48 | # The main reason for choosing this workaround for aborting the workflow is the fact that it does not display the workflow as successful, which can set false expectations. 49 | - name: DOCKERFILE_PATH. 50 | shell: bash 51 | run: | 52 | # We check whether the Dockerfile_path is valid. 53 | if [ ! -f ${{ github.event.inputs.DOCKERFILE_PATH }} ]; then 54 | echo "::error title=Invalid Dockerfile path::No file found at ${{ github.event.inputs.DOCKERFILE_PATH }}" 55 | echo "terminate=true" >> $GITHUB_ENV 56 | fi 57 | 58 | - name: Github Pages config guard clause 59 | if: ${{ github.event.inputs.DEPLOY_TO_GITHUB_PAGES == 'true' }} 60 | run: | 61 | # We use the Github Rest api to get information regarding pages for the Github Repository and store it into a temporary file named "pages_response". 62 | set +e 63 | gh api \ 64 | -H "Accept: application/vnd.github+json" \ 65 | -H "X-GitHub-Api-Version: 2022-11-28" \ 66 | /repos/${{ github.repository_owner }}/$(basename ${{ github.repository }})/pages > pages_response 67 | 68 | # We make sure Github Pages has been enabled for this repository. 69 | if [ "$?" -ne 0 ]; then 70 | echo "::error title=Potential pages configuration error.::Please make sure you have enabled Github pages for the ${{ github.repository }} repository. If already enabled then Github pages might be down" 71 | echo "terminate=true" >> $GITHUB_ENV 72 | fi 73 | set -e 74 | 75 | # We make sure the Github pages build & deployment source is set to "workflow" (Github Actions). Instead of a "legacy" (branch). 76 | if [[ "$(jq --compact-output --raw-output .build_type pages_response)" != "workflow" ]]; then 77 | echo "Undefined behaviour, Make sure the Github Pages source is correctly configured in the Github Pages settings." 78 | echo "::error title=Pages configuration error.::Please make sure you have correctly picked \"Github Actions\" as the build and deployment source for the Github Pages." 79 | echo "terminate=true" >> $GITHUB_ENV 80 | fi 81 | rm pages_response 82 | 83 | - name: Terminate run if error occurred. 84 | run: | 85 | if [[ $terminate == "true" ]]; then 86 | gh run cancel ${{ github.run_id }} 87 | gh run watch ${{ github.run_id }} 88 | fi 89 | 90 | build: 91 | needs: guard_clause # Dependency 92 | runs-on: ubuntu-latest # Image to run the worker on. 93 | 94 | env: 95 | TAG: "ext2-webvm-base-image" # Tag of docker image. 96 | IMAGE_SIZE: '${{ github.event.inputs.IMAGE_SIZE }}' 97 | DEPLOY_DIR: /webvm_deploy/ # Path to directory where we host the final image from. 98 | 99 | permissions: # Permissions to grant the GITHUB_TOKEN. 100 | contents: write # Required permission to make a github release. 101 | 102 | steps: 103 | # Checks-out our repository under $GITHUB_WORKSPACE, so our job can access it 104 | - uses: actions/checkout@v4 105 | 106 | # Setting the IMAGE_NAME variable in GITHUB_ENV to __.ext2. 107 | - name: Generate the image_name. 108 | id: image_name_gen 109 | run: | 110 | echo "IMAGE_NAME=$(basename ${{ github.event.inputs.DOCKERFILE_PATH }})_$(date +%Y%m%d)_${{ github.run_id }}.ext2" >> $GITHUB_ENV 111 | 112 | # Create directory to host the image from. 113 | - run: sudo mkdir -p $DEPLOY_DIR 114 | 115 | # Build the i386 Dockerfile image. 116 | - run: docker build . --tag $TAG --file ${{ github.event.inputs.DOCKERFILE_PATH }} --platform=i386 117 | 118 | # Run the docker image so that we can export the container. 119 | # Run the Docker container with the Google Public DNS nameservers: 8.8.8.8, 8.8.4.4 120 | - run: | 121 | docker run --dns 8.8.8.8 --dns 8.8.4.4 -d $TAG 122 | echo "CONTAINER_ID=$(sudo docker ps -aq)" >> $GITHUB_ENV 123 | 124 | # We extract the CMD, we first need to figure whether the Dockerfile uses CMD or an Entrypoint. 125 | - name: Extracting CMD / Entrypoint and args 126 | shell: bash 127 | run: | 128 | cmd=$(sudo docker inspect --format='{{json .Config.Cmd}}' $CONTAINER_ID) 129 | entrypoint=$(sudo docker inspect --format='{{json .Config.Entrypoint}}' $CONTAINER_ID) 130 | if [[ $entrypoint != "null" && $cmd != "null" ]]; then 131 | echo "CMD=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.Entrypoint' )" >> $GITHUB_ENV 132 | echo "ARGS=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.Cmd' )" >> $GITHUB_ENV 133 | elif [[ $cmd != "null" ]]; then 134 | echo "CMD=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.Cmd[:1]' )" >> $GITHUB_ENV 135 | echo "ARGS=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.Cmd[1:]' )" >> $GITHUB_ENV 136 | else 137 | echo "CMD=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.Entrypoint[:1]' )" >> $GITHUB_ENV 138 | echo "ARGS=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.Entrypoint[1:]' )" >> $GITHUB_ENV 139 | fi 140 | 141 | # We extract the ENV, CMD/Entrypoint and cwd from the Docker container with docker inspect. 142 | - name: Extracting env, args and cwd. 143 | shell: bash 144 | run: | 145 | echo "ENV=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.Env' )" >> $GITHUB_ENV 146 | echo "CWD=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.WorkingDir' )" >> $GITHUB_ENV 147 | 148 | # We create and mount the base ext2 image to extract the Docker container's filesystem its contents into. 149 | - name: Create ext2 image. 150 | run: | 151 | # Preallocate space for the ext2 image 152 | sudo fallocate -l $IMAGE_SIZE ${IMAGE_NAME} 153 | # Format to ext2 linux kernel revision 0 154 | sudo mkfs.ext2 -r 0 ${IMAGE_NAME} 155 | # Mount the ext2 image to modify it 156 | sudo mount -o loop -t ext2 ${IMAGE_NAME} /mnt/ 157 | 158 | # We opt for 'docker cp --archive' over 'docker save' since our focus is solely on the end product rather than individual layers and metadata. 159 | # However, it's important to note that despite being specified in the documentation, the '--archive' flag does not currently preserve uid/gid information when copying files from the container to the host machine. 160 | # Another compelling reason to use 'docker cp' is that it preserves resolv.conf. 161 | - name: Export and unpack container filesystem contents into mounted ext2 image. 162 | run: | 163 | sudo docker cp -a ${CONTAINER_ID}:/ /mnt/ 164 | sudo umount /mnt/ 165 | # Result is an ext2 image for webvm. 166 | 167 | # The .txt suffix enabled HTTP compression for free 168 | - name: Generate image split chunks and .meta file 169 | run: | 170 | sudo split ${{ env.IMAGE_NAME }} ${{ env.DEPLOY_DIR }}/${{ env.IMAGE_NAME }}.c -a 6 -b 128k -x --additional-suffix=.txt 171 | sudo bash -c "stat -c%s ${{ env.IMAGE_NAME }} > ${{ env.DEPLOY_DIR }}/${{ env.IMAGE_NAME }}.meta" 172 | 173 | # This step updates the default config_github_terminal.js file by performing the following actions: 174 | # 1. Replaces all occurrences of IMAGE_URL with the URL to the image. 175 | # 2. Replace CMD with the Dockerfile entry command. 176 | # 3. Replace args with the Dockerfile CMD / Entrypoint args. 177 | # 4. Replace ENV with the container's environment values. 178 | # 5. Replace CWD with the container's current working directory. 179 | - name: Adjust config_github_terminal.js 180 | run: | 181 | sed -i 's#IMAGE_URL#"${{ env.IMAGE_NAME }}"#g' config_github_terminal.js 182 | sed -i 's#CMD#${{ env.CMD }}#g' config_github_terminal.js 183 | sed -i 's#ARGS#${{ env.ARGS }}#g' config_github_terminal.js 184 | sed -i 's#ENV#${{ env.ENV }}#g' config_github_terminal.js 185 | sed -i 's#CWD#${{ env.CWD }}#g' config_github_terminal.js 186 | 187 | - name: Build NPM package 188 | run: | 189 | npm install 190 | WEBVM_MODE=github npm run build 191 | 192 | # Move required files for gh-pages deployment to the deployment directory $DEPLOY_DIR. 193 | - name: Copy build 194 | run: | 195 | rm build/alpine.html 196 | sudo mv build/* $DEPLOY_DIR/ 197 | 198 | # We generate index.list files for our httpfs to function properly. 199 | - name: make index.list 200 | shell: bash 201 | run: | 202 | find $DEPLOY_DIR -type d | while read -r dir; 203 | do 204 | index_list="$dir/index.list"; 205 | sudo rm -f "$index_list"; 206 | sudo ls "$dir" | sudo tee "$index_list" > /dev/null; 207 | sudo chmod +rw "$index_list"; 208 | sudo echo "created $index_list"; 209 | done 210 | 211 | # Create a gh-pages artifact in order to deploy to gh-pages. 212 | - name: Upload GitHub Pages artifact 213 | uses: actions/upload-pages-artifact@v3 214 | with: 215 | # Path of the directory containing the static assets for our gh pages deployment. 216 | path: ${{ env.DEPLOY_DIR }} # optional, default is _site/ 217 | 218 | - name: github release # To upload our final ext2 image as a github release. 219 | if: ${{ github.event.inputs.GITHUB_RELEASE == 'true' }} 220 | uses: softprops/action-gh-release@v2 221 | with: 222 | target_commitish: ${{ github.sha }} # Last commit on the GITHUB_REF branch or tag 223 | tag_name: ext2_image 224 | fail_on_unmatched_files: 'true' # Fail in case of no matches with the file(s) glob(s). 225 | files: | # Assets to upload as release. 226 | ${{ env.IMAGE_NAME }} 227 | 228 | deploy_to_github_pages: # Job that deploys the github-pages artifact to github-pages. 229 | if: ${{ github.event.inputs.DEPLOY_TO_GITHUB_PAGES == 'true' }} 230 | needs: build 231 | environment: 232 | name: github-pages 233 | url: ${{ steps.deployment.outputs.page_url }} 234 | 235 | # Grant GITHUB_TOKEN the permissions required to make a Pages deployment 236 | permissions: 237 | pages: write # to deploy to Pages 238 | id-token: write # to verify the deployment originates from an appropriate source 239 | 240 | runs-on: ubuntu-latest 241 | steps: 242 | # Deployment to github pages 243 | - name: Deploy GitHub Pages site 244 | id: deployment 245 | uses: actions/deploy-pages@v4 246 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.svelte-kit 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebVM 2 | 3 | [![Discord server](https://img.shields.io/discord/988743885121548329?color=%235865F2&logo=discord&logoColor=%23fff)](https://discord.gg/yWRr2YnD9c) 4 | [![Issues](https://img.shields.io/github/issues/leaningtech/webvm)](https://github.com/leaningtech/webvm/issues) 5 | 6 | This repository hosts the source code for [https://webvm.io](https://webvm.io), a Linux virtual machine that runs in your browser. 7 | 8 | Try out the new Alpine / Xorg / i3 graphical environment: [https://webvm.io/alpine.html](https://webvm.io/alpine.html) 9 | 10 | 11 | 12 | WebVM is a server-less virtual environment running fully client-side in HTML5/WebAssembly. It's designed to be Linux ABI-compatible. It runs an unmodified Debian distribution including many native development toolchains. 13 | 14 | WebVM is powered by the CheerpX virtualization engine, and enables safe, sandboxed client-side execution of x86 binaries on any browser. CheerpX includes an x86-to-WebAssembly JIT compiler, a virtual block-based file system, and a Linux syscall emulator. 15 | 16 | # Enable networking 17 | 18 | Modern browsers do not provide APIs to directly use TCP or UDP. WebVM provides networking support by integrating with Tailscale, a VPN network that supports WebSockets as a transport layer. 19 | 20 | - Open the "Networking" panel from the side-bar 21 | - Click "Connect to Tailscale" from the panel 22 | - Log in to Tailscale (create an account if you don't have one) 23 | - Click "Connect" when prompted by Tailscale 24 | - If you are unfamiliar with Tailscale or would like additional information see [WebVM and Tailscale](/docs/Tailscale.md). 25 | 26 | # Fork, deploy, customize 27 | 28 | deploy_instructions_gif 29 | 30 | - Fork the repository. 31 | - Enable Github pages in settings. 32 | - Click on `Settings`. 33 | - Go to the `Pages` section. 34 | - Select `Github Actions` as the source. 35 | - If you are using a custom domain, ensure `Enforce HTTPS` is enabled. 36 | - Run the workflow. 37 | - Click on `Actions`. 38 | - Accept the prompt. This is required only once to enable Actions for your fork. 39 | - Click on the workflow named `Deploy`. 40 | - Click `Run workflow` and then once more `Run workflow` in the menu. 41 | - After a few seconds a new `Deploy` workflow will start, click on it to see details. 42 | - After the workflow completes, which takes a few minutes, it will show the URL below the `deploy_to_github_pages` job. 43 | 44 | 45 | 46 | You can now customize `dockerfiles/debian_mini` to suit your needs, or make a new Dockerfile from scratch. Use the `Path to Dockerfile` workflow parameter to select it. 47 | 48 | # Run WebVM locally with a custom Debian mini disk image 49 | 50 | 1. Clone the WebVM Repository 51 | 52 | ```sh 53 | git clone https://github.com/leaningtech/webvm.git 54 | cd webvm 55 | ``` 56 | 57 | 2. Download the Debian mini Ext2 image 58 | 59 | Run the following command to download the Debian mini Ext2 image: 60 | 61 | ```sh 62 | wget "https://github.com/leaningtech/webvm/releases/download/ext2_image/debian_mini_20230519_5022088024.ext2" 63 | ``` 64 | 65 | (*You can also build your own disk image by selecting the **"Upload GitHub release"** workflow option*) 66 | 67 | 3. Update the configuration file 68 | 69 | Edit `config_public_terminal.js` to reference your local disk image: 70 | 71 | - Replace: 72 | 73 | `"wss://disks.webvm.io/debian_large_20230522_5044875331.ext2"` 74 | 75 | With: 76 | 77 | `"/disk-images/debian_mini_20230519_5022088024.ext2"` 78 | 79 | (*Use an absolute or relative URL pointing to the disk image location.*) 80 | 81 | 82 | - Replace `"cloud"` with the correct disk image type: `"bytes"` 83 | 84 | 4. Build WebVM 85 | 86 | Run the following commands to install dependencies and build WebVM: 87 | 88 | ```sh 89 | npm install 90 | npm run build 91 | ``` 92 | 93 | The output will be placed in the `build` directory. 94 | 95 | 5. Configure Nginx 96 | 97 | - Create a directory for the disk image: 98 | 99 | ```sh 100 | mkdir disk-images 101 | mv debian_mini_20230519_5022088024.ext2 disk-images/ 102 | ``` 103 | 104 | - Modify your `nginx.conf` file to serve the disk image. Add the following location block: 105 | 106 | ```nginx 107 | location /disk-images/ { 108 | root .; 109 | autoindex on; 110 | } 111 | ``` 112 | 113 | 6. Start Nginx 114 | 115 | Run the following command to start Nginx: 116 | 117 | ```sh 118 | nginx -p . -c nginx.conf 119 | ``` 120 | 121 | *Nginx will automatically serve the build directory.* 122 | 123 | 7. Access WebVM 124 | 125 | Open a browser and visit: `http://127.0.0.1:8081`. 126 | 127 | Enjoy your local WebVM! 128 | 129 | # Example customization: Python3 REPL 130 | 131 | The `Deploy` workflow takes into account the `CMD` specified in the Dockerfile. To build a REPL you can simply apply this patch and deploy. 132 | 133 | ```diff 134 | diff --git a/dockerfiles/debian_mini b/dockerfiles/debian_mini 135 | index 2878332..1f3103a 100644 136 | --- a/dockerfiles/debian_mini 137 | +++ b/dockerfiles/debian_mini 138 | @@ -15,4 +15,4 @@ WORKDIR /home/user/ 139 | # We set env, as this gets extracted by Webvm. This is optional. 140 | ENV HOME="/home/user" TERM="xterm" USER="user" SHELL="/bin/bash" EDITOR="vim" LANG="en_US.UTF-8" LC_ALL="C" 141 | RUN echo 'root:password' | chpasswd 142 | -CMD [ "/bin/bash" ] 143 | +CMD [ "/usr/bin/python3" ] 144 | ``` 145 | 146 | # How to use Claude AI 147 | 148 | To access Claude AI, you need an API key. Follow these steps to get started: 149 | 150 | 1. Create an account 151 | - Visit [Anthropic Console](https://console.anthropic.com/login) and sign up with your e-mail. You'll receive a sign in link to the Anthropic Console. 152 | 153 | 154 | 155 | 2. Get your API key 156 | - Once logged in, navigate to **Get API keys**. 157 | - Purchase the amount of credits you need. After completing the purchase, you'll be able to generate the key through the API console. 158 | 159 | 160 | 161 | 3. Log in with your API key 162 | - Navigate to your WebVM and hover over the robot icon. This will show the Claude AI Integration tab. For added convenience, you can click the pin button in the top right corner to keep the tab in place. 163 | - You'll see a prompt where you can insert your Claude API key. 164 | - Insert your key and press enter. 165 | 166 | 167 | 168 | 4. Start using Claude AI 169 | - Once your API key is entered, you can begin interacting with Claude AI by asking questions such as: 170 | 171 | __"Solve the CTF challenge at `/home/user/chall1.bin.` Note that the binary reads from stdin."__ 172 | 173 | deploy_instructions_gif 174 | 175 | **Important:** Your API key is private and should never be shared. We do not have access to your key, which is not only stored locally in your browser. 176 | 177 | # Bugs and Issues 178 | 179 | Please use [Issues](https://github.com/leaningtech/webvm/issues) to report any bug. 180 | Or come to say hello / share your feedback on [Discord](https://discord.gg/yTNZgySKGa). 181 | 182 | # More links 183 | 184 | - [WebVM: server-less x86 virtual machines in the browser](https://leaningtech.com/webvm-server-less-x86-virtual-machines-in-the-browser/) 185 | - [WebVM: Linux Virtualization in WebAssembly with Full Networking via Tailscale](https://leaningtech.com/webvm-virtual-machine-with-networking-via-tailscale/) 186 | - [Mini.WebVM: Your own Linux box from Dockerfile, virtualized in the browser via WebAssembly](https://leaningtech.com/mini-webvm-your-linux-box-from-dockerfile-via-wasm/) 187 | - Reference GitHub Pages deployment: [Mini.WebVM](https://mini.webvm.io) 188 | - [Crafting the Impossible: X86 Virtualization in the Browser with WebAssembly](https://www.youtube.com/watch?v=VqrbVycTXmw) Talk at JsNation 2022 189 | 190 | # Thanks to... 191 | This project depends on: 192 | - [CheerpX](https://cheerpx.io/), made by [Leaning Technologies](https://leaningtech.com/) for x86 virtualization and Linux emulation 193 | - xterm.js, [https://xtermjs.org/](https://xtermjs.org/), for providing the Web-based terminal emulator 194 | - [Tailscale](https://tailscale.com/), for the networking component 195 | - [lwIP](https://savannah.nongnu.org/projects/lwip/), for the TCP/IP stack, compiled for the Web via [Cheerp](https://github.com/leaningtech/cheerp-meta/) 196 | 197 | # Versioning 198 | 199 | WebVM depends on the CheerpX x86-to-WebAssembly virtualization technology, which is included in the project via [NPM](https://www.npmjs.com/package/@leaningtech/cheerpx). 200 | 201 | The NPM package is updated on every release. 202 | 203 | Every build is immutable, if a specific version works well for you today, it will keep working forever. 204 | 205 | # License 206 | 207 | WebVM is released under the Apache License, Version 2.0. 208 | 209 | You are welcome to use, modify, and redistribute the contents of this repository. 210 | 211 | The public CheerpX deployment is provided **as-is** and is **free to use** for technological exploration, testing and use by individuals. Any other use by organizations, including non-profit, academia and the public sector, requires a license. Downloading a CheerpX build for the purpose of hosting it elsewhere is not permitted without a commercial license. 212 | 213 | Read more about [CheerpX licensing](https://cheerpx.io/docs/licensing) 214 | 215 | If you want to build a product on top of CheerpX/WebVM, please get in touch: sales@leaningtech.com 216 | -------------------------------------------------------------------------------- /assets/alpine_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leaningtech/webvm/e8ead6001d6453b4eeced2b60fddd6c8b85e89e8/assets/alpine_bg.png -------------------------------------------------------------------------------- /assets/anthropic_api_payment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leaningtech/webvm/e8ead6001d6453b4eeced2b60fddd6c8b85e89e8/assets/anthropic_api_payment.png -------------------------------------------------------------------------------- /assets/anthropic_signup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leaningtech/webvm/e8ead6001d6453b4eeced2b60fddd6c8b85e89e8/assets/anthropic_signup.png -------------------------------------------------------------------------------- /assets/cheerpx.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/discord-mark-blue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/fork_deploy_instructions.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leaningtech/webvm/e8ead6001d6453b4eeced2b60fddd6c8b85e89e8/assets/fork_deploy_instructions.gif -------------------------------------------------------------------------------- /assets/github-mark-white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/insert_key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leaningtech/webvm/e8ead6001d6453b4eeced2b60fddd6c8b85e89e8/assets/insert_key.png -------------------------------------------------------------------------------- /assets/leaningtech.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leaningtech/webvm/e8ead6001d6453b4eeced2b60fddd6c8b85e89e8/assets/leaningtech.png -------------------------------------------------------------------------------- /assets/result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leaningtech/webvm/e8ead6001d6453b4eeced2b60fddd6c8b85e89e8/assets/result.png -------------------------------------------------------------------------------- /assets/social_2024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leaningtech/webvm/e8ead6001d6453b4eeced2b60fddd6c8b85e89e8/assets/social_2024.png -------------------------------------------------------------------------------- /assets/tailscale.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /assets/webvm_claude_ctf.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leaningtech/webvm/e8ead6001d6453b4eeced2b60fddd6c8b85e89e8/assets/webvm_claude_ctf.gif -------------------------------------------------------------------------------- /assets/webvm_hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leaningtech/webvm/e8ead6001d6453b4eeced2b60fddd6c8b85e89e8/assets/webvm_hero.png -------------------------------------------------------------------------------- /assets/welcome_to_WebVM_2024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leaningtech/webvm/e8ead6001d6453b4eeced2b60fddd6c8b85e89e8/assets/welcome_to_WebVM_2024.png -------------------------------------------------------------------------------- /assets/welcome_to_WebVM_alpine_2024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leaningtech/webvm/e8ead6001d6453b4eeced2b60fddd6c8b85e89e8/assets/welcome_to_WebVM_alpine_2024.png -------------------------------------------------------------------------------- /config_github_terminal.js: -------------------------------------------------------------------------------- 1 | // The root filesystem location 2 | export const diskImageUrl = IMAGE_URL; 3 | // The root filesystem backend type 4 | export const diskImageType = "github"; 5 | // Print an introduction message about the technology 6 | export const printIntro = true; 7 | // Is a graphical display needed 8 | export const needsDisplay = false; 9 | // Executable full path (Required) 10 | export const cmd = CMD; // Default: "/bin/bash"; 11 | // Arguments, as an array (Required) 12 | export const args = ARGS; // Default: ["--login"]; 13 | // Optional extra parameters 14 | export const opts = { 15 | // Environment variables 16 | env: ENV, // Default: ["HOME=/home/user", "TERM=xterm", "USER=user", "SHELL=/bin/bash", "EDITOR=vim", "LANG=en_US.UTF-8", "LC_ALL=C"], 17 | // Current working directory 18 | cwd: CWD, // Default: "/home/user", 19 | // User id 20 | uid: 1000, 21 | // Group id 22 | gid: 1000 23 | }; 24 | -------------------------------------------------------------------------------- /config_public_alpine.js: -------------------------------------------------------------------------------- 1 | // The root filesystem location 2 | export const diskImageUrl = "wss://disks.webvm.io/alpine_20250305.ext2"; 3 | // The root filesystem backend type 4 | export const diskImageType = "cloud"; 5 | // Print an introduction message about the technology 6 | export const printIntro = false; 7 | // Is a graphical display needed 8 | export const needsDisplay = true; 9 | // Executable full path (Required) 10 | export const cmd = "/sbin/init"; 11 | // Arguments, as an array (Required) 12 | export const args = []; 13 | // Optional extra parameters 14 | export const opts = { 15 | // User id 16 | uid: 0, 17 | // Group id 18 | gid: 0 19 | }; 20 | -------------------------------------------------------------------------------- /config_public_terminal.js: -------------------------------------------------------------------------------- 1 | // The root filesystem location 2 | export const diskImageUrl = "wss://disks.webvm.io/debian_large_20230522_5044875331.ext2"; 3 | // The root filesystem backend type 4 | export const diskImageType = "cloud"; 5 | // Print an introduction message about the technology 6 | export const printIntro = true; 7 | // Is a graphical display needed 8 | export const needsDisplay = false; 9 | // Executable full path (Required) 10 | export const cmd = "/bin/bash"; 11 | // Arguments, as an array (Required) 12 | export const args = ["--login"]; 13 | // Optional extra parameters 14 | export const opts = { 15 | // Environment variables 16 | env: ["HOME=/home/user", "TERM=xterm", "USER=user", "SHELL=/bin/bash", "EDITOR=vim", "LANG=en_US.UTF-8", "LC_ALL=C"], 17 | // Current working directory 18 | cwd: "/home/user", 19 | // User id 20 | uid: 1000, 21 | // Group id 22 | gid: 1000 23 | }; 24 | -------------------------------------------------------------------------------- /dockerfiles/.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | -------------------------------------------------------------------------------- /dockerfiles/debian_large: -------------------------------------------------------------------------------- 1 | FROM --platform=i386 i386/debian:buster 2 | ARG DEBIAN_FRONTEND=noninteractive 3 | 4 | RUN apt-get update && apt-get -y upgrade && \ 5 | apt-get install -y apt-utils beef bsdgames bsdmainutils ca-certificates clang \ 6 | cowsay cpio cron curl dmidecode dmsetup g++ gcc gdbm-l10n git \ 7 | hexedit ifupdown init logrotate lsb-base lshw lua50 luajit lynx make \ 8 | nano netbase nodejs openssl procps python3 python3-cryptography \ 9 | python3-jinja2 python3-numpy python3-pandas python3-pip python3-scipy \ 10 | python3-six python3-yaml readline-common rsyslog ruby sensible-utils \ 11 | ssh systemd systemd-sysv tasksel tasksel-data udev vim wget whiptail \ 12 | xxd iptables isc-dhcp-client isc-dhcp-common kmod less netcat-openbsd 13 | 14 | # Make a user, then copy over the /example directory 15 | RUN useradd -m user && echo "user:password" | chpasswd 16 | COPY --chown=user:user ./examples /home/user/examples 17 | RUN chmod -R +x /home/user/examples/lua 18 | RUN echo 'root:password' | chpasswd 19 | CMD [ "/bin/bash" ] 20 | -------------------------------------------------------------------------------- /dockerfiles/debian_mini: -------------------------------------------------------------------------------- 1 | FROM --platform=i386 i386/debian:buster 2 | ARG DEBIAN_FRONTEND=noninteractive 3 | RUN apt-get clean && apt-get update && apt-get -y upgrade 4 | RUN apt-get -y install apt-utils gcc \ 5 | python3 vim unzip ruby nodejs \ 6 | fakeroot dbus base whiptail hexedit \ 7 | patch wamerican ucf manpages \ 8 | file luajit make lua50 dialog curl \ 9 | less cowsay netcat-openbsd 10 | RUN useradd -m user && echo "user:password" | chpasswd 11 | COPY --chown=user:user ./examples /home/user/examples 12 | RUN chmod -R +x /home/user/examples/lua 13 | # We set WORKDIR, as this gets extracted by Webvm to be used as the cwd. This is optional. 14 | WORKDIR /home/user/ 15 | # We set env, as this gets extracted by Webvm. This is optional. 16 | ENV HOME="/home/user" TERM="xterm" USER="user" SHELL="/bin/bash" EDITOR="vim" LANG="en_US.UTF-8" LC_ALL="C" 17 | RUN echo 'root:password' | chpasswd 18 | CMD [ "/bin/bash" ] 19 | -------------------------------------------------------------------------------- /docs/Tailscale.md: -------------------------------------------------------------------------------- 1 | # Enable networking 2 | 3 | - In order to access the public internet, you will need an Exit Node. See [Tailscale Exit Nodes](https://tailscale.com/kb/1103/exit-nodes/) for detailed instructions. 4 | - ***Note:*** This is not required to access machines in your own Tailscale Network. 5 | - Depending on your network speed, you may need to wait a few moments for the Tailscale Wasm module to be downloaded. 6 | 7 | **When all set:** 8 | - Log in with your Tailscale credentials. 9 | - Go back to the WebVM tab. 10 | - The `Connect to Tailscale` button in the Networking side-panel should be replaced by your IP address. 11 | 12 | # Log in to Tailscale with an Auth key 13 | 14 | - Add `#authKey=` at the end of the URL. 15 | - Done, you don't need to manually log in anymore. 16 | 17 | It is recommended to use an ephemeral key. 18 | 19 | # Log in to a self-hosted Tailscale network (Headscale) 20 | 21 | - Add `#controlUrl=` at the end of the URL. 22 | - You can combine this option with `authKey` with a `&`: `#controlUrl=&authKey=`. 23 | -------------------------------------------------------------------------------- /documents/ArchitectureOverview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leaningtech/webvm/e8ead6001d6453b4eeced2b60fddd6c8b85e89e8/documents/ArchitectureOverview.png -------------------------------------------------------------------------------- /documents/WebAssemblyTools.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leaningtech/webvm/e8ead6001d6453b4eeced2b60fddd6c8b85e89e8/documents/WebAssemblyTools.pdf -------------------------------------------------------------------------------- /documents/Welcome.txt: -------------------------------------------------------------------------------- 1 | Welcome to WebVM: A complete desktop environment running in the browser 2 | 3 | WebVM is powered by CheerpX: a x86-to-WebAssembly virtualization engine and Just-in-Time compiler 4 | 5 | For more info: https://cheerpx.io 6 | -------------------------------------------------------------------------------- /documents/index.list: -------------------------------------------------------------------------------- 1 | ArchitectureOverview.png 2 | WebAssemblyTools.pdf 3 | Welcome.txt 4 | -------------------------------------------------------------------------------- /examples/c/Makefile: -------------------------------------------------------------------------------- 1 | SRCS = $(wildcard *.c) 2 | 3 | PROGS = $(patsubst %.c,%,$(SRCS)) 4 | 5 | all: $(PROGS) 6 | 7 | %: %.c 8 | $(CC) $(CFLAGS) -o $@ $< 9 | 10 | clean: 11 | rm -f $(PROGS) 12 | 13 | .PHONY: all clean 14 | -------------------------------------------------------------------------------- /examples/c/env.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | // Most of the C compilers support a third parameter to main which 4 | // store all envorinment variables 5 | int main(int argc, char *argv[], char * envp[]) 6 | { 7 | int i; 8 | for (i = 0; envp[i] != NULL; i++) 9 | printf("\n%s", envp[i]); 10 | getchar(); 11 | return 0; 12 | } 13 | -------------------------------------------------------------------------------- /examples/c/helloworld.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() 4 | { 5 | printf("Hello, World!\n"); 6 | } 7 | -------------------------------------------------------------------------------- /examples/c/link.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() 4 | { 5 | link("env", "env3"); 6 | return 0; 7 | } 8 | -------------------------------------------------------------------------------- /examples/c/openat.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | int main() 6 | { 7 | int ret = openat(AT_FDCWD, "/dev/tty", 0x88102, 0); 8 | printf("return value is %d and errno is %d\n", ret, errno); 9 | } 10 | 11 | -------------------------------------------------------------------------------- /examples/c/waitpid.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | int main() 7 | { 8 | int status; 9 | 10 | pid_t p = getpid(); 11 | // waitpid takes a children's pid, not the current process one 12 | // if the pid is not a children of the current process, it returns -ECHILD 13 | pid_t res = waitpid(1001, &status, WNOHANG); 14 | 15 | printf("res is %d, p is %d and errno is %d\n", res, p, errno); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /examples/lua/fizzbuzz.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env luajit 2 | cfizz,cbuzz=0,0 3 | for i=1,20 do 4 | cfizz=cfizz+1 5 | cbuzz=cbuzz+1 6 | io.write(i .. ": ") 7 | if cfizz~=3 and cbuzz~=5 then 8 | io.write(i) 9 | else 10 | if cfizz==3 then 11 | io.write("Fizz") 12 | cfizz=0 13 | end 14 | if cbuzz==5 then 15 | io.write("Buzz") 16 | cbuzz=0 17 | end 18 | end 19 | io.write("\n") 20 | end 21 | -------------------------------------------------------------------------------- /examples/lua/sorting.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env luajit 2 | fruits = {"banana","orange","apple","grapes"} 3 | 4 | for k,v in ipairs(fruits) do 5 | print(k,v) 6 | end 7 | 8 | table.sort(fruits) 9 | print("sorted table") 10 | 11 | for k,v in ipairs(fruits) do 12 | print(k,v) 13 | end 14 | -------------------------------------------------------------------------------- /examples/lua/symmetric_difference.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env luajit 2 | A = { ["John"] = true, ["Bob"] = true, ["Mary"] = true, ["Elena"] = true } 3 | B = { ["Jim"] = true, ["Mary"] = true, ["John"] = true, ["Bob"] = true } 4 | 5 | A_B = {} 6 | for a in pairs(A) do 7 | if not B[a] then A_B[a] = true end 8 | end 9 | 10 | B_A = {} 11 | for b in pairs(B) do 12 | if not A[b] then B_A[b] = true end 13 | end 14 | 15 | for a_b in pairs(A_B) do 16 | print( a_b ) 17 | end 18 | for b_a in pairs(B_A) do 19 | print( b_a ) 20 | end 21 | -------------------------------------------------------------------------------- /examples/nodejs/environment.js: -------------------------------------------------------------------------------- 1 | console.log("process.uptime = ", global.process.uptime()); 2 | console.log("process.title = ", global.process.title); 3 | console.log("process version = ", global.process.version); 4 | console.log("process.platform = ", global.process.platform); 5 | console.log("process.cwd = ", global.process.cwd()); 6 | console.log("process.uptime = ", global.process.uptime()); 7 | -------------------------------------------------------------------------------- /examples/nodejs/nbody.js: -------------------------------------------------------------------------------- 1 | const PI = Math.PI; 2 | const SOLAR_MASS = 4 * PI * PI; 3 | const DAYS_PER_YEAR = 365.24; 4 | 5 | function Body(x, y, z, vx, vy, vz, mass) { 6 | this.x = x; 7 | this.y = y; 8 | this.z = z; 9 | this.vx = vx; 10 | this.vy = vy; 11 | this.vz = vz; 12 | this.mass = mass; 13 | } 14 | 15 | function Jupiter() { 16 | return new Body( 17 | 4.84143144246472090e+00, 18 | -1.16032004402742839e+00, 19 | -1.03622044471123109e-01, 20 | 1.66007664274403694e-03 * DAYS_PER_YEAR, 21 | 7.69901118419740425e-03 * DAYS_PER_YEAR, 22 | -6.90460016972063023e-05 * DAYS_PER_YEAR, 23 | 9.54791938424326609e-04 * SOLAR_MASS 24 | ); 25 | } 26 | 27 | function Saturn() { 28 | return new Body( 29 | 8.34336671824457987e+00, 30 | 4.12479856412430479e+00, 31 | -4.03523417114321381e-01, 32 | -2.76742510726862411e-03 * DAYS_PER_YEAR, 33 | 4.99852801234917238e-03 * DAYS_PER_YEAR, 34 | 2.30417297573763929e-05 * DAYS_PER_YEAR, 35 | 2.85885980666130812e-04 * SOLAR_MASS 36 | ); 37 | } 38 | 39 | function Uranus() { 40 | return new Body( 41 | 1.28943695621391310e+01, 42 | -1.51111514016986312e+01, 43 | -2.23307578892655734e-01, 44 | 2.96460137564761618e-03 * DAYS_PER_YEAR, 45 | 2.37847173959480950e-03 * DAYS_PER_YEAR, 46 | -2.96589568540237556e-05 * DAYS_PER_YEAR, 47 | 4.36624404335156298e-05 * SOLAR_MASS 48 | ); 49 | } 50 | 51 | function Neptune() { 52 | return new Body( 53 | 1.53796971148509165e+01, 54 | -2.59193146099879641e+01, 55 | 1.79258772950371181e-01, 56 | 2.68067772490389322e-03 * DAYS_PER_YEAR, 57 | 1.62824170038242295e-03 * DAYS_PER_YEAR, 58 | -9.51592254519715870e-05 * DAYS_PER_YEAR, 59 | 5.15138902046611451e-05 * SOLAR_MASS 60 | ); 61 | } 62 | 63 | function Sun() { 64 | return new Body(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, SOLAR_MASS); 65 | } 66 | 67 | const bodies = Array(Sun(), Jupiter(), Saturn(), Uranus(), Neptune()); 68 | 69 | function offsetMomentum() { 70 | let px = 0; 71 | let py = 0; 72 | let pz = 0; 73 | const size = bodies.length; 74 | for (let i = 0; i < size; i++) { 75 | const body = bodies[i]; 76 | const mass = body.mass; 77 | px += body.vx * mass; 78 | py += body.vy * mass; 79 | pz += body.vz * mass; 80 | } 81 | 82 | const body = bodies[0]; 83 | body.vx = -px / SOLAR_MASS; 84 | body.vy = -py / SOLAR_MASS; 85 | body.vz = -pz / SOLAR_MASS; 86 | } 87 | 88 | function advance(dt) { 89 | const size = bodies.length; 90 | 91 | for (let i = 0; i < size; i++) { 92 | const bodyi = bodies[i]; 93 | let vxi = bodyi.vx; 94 | let vyi = bodyi.vy; 95 | let vzi = bodyi.vz; 96 | for (let j = i + 1; j < size; j++) { 97 | const bodyj = bodies[j]; 98 | const dx = bodyi.x - bodyj.x; 99 | const dy = bodyi.y - bodyj.y; 100 | const dz = bodyi.z - bodyj.z; 101 | 102 | const d2 = dx * dx + dy * dy + dz * dz; 103 | const mag = dt / (d2 * Math.sqrt(d2)); 104 | 105 | const massj = bodyj.mass; 106 | vxi -= dx * massj * mag; 107 | vyi -= dy * massj * mag; 108 | vzi -= dz * massj * mag; 109 | 110 | const massi = bodyi.mass; 111 | bodyj.vx += dx * massi * mag; 112 | bodyj.vy += dy * massi * mag; 113 | bodyj.vz += dz * massi * mag; 114 | } 115 | bodyi.vx = vxi; 116 | bodyi.vy = vyi; 117 | bodyi.vz = vzi; 118 | } 119 | 120 | for (let i = 0; i < size; i++) { 121 | const body = bodies[i]; 122 | body.x += dt * body.vx; 123 | body.y += dt * body.vy; 124 | body.z += dt * body.vz; 125 | } 126 | } 127 | 128 | function energy() { 129 | let e = 0; 130 | const size = bodies.length; 131 | 132 | for (let i = 0; i < size; i++) { 133 | const bodyi = bodies[i]; 134 | 135 | e += 0.5 * bodyi.mass * ( bodyi.vx * bodyi.vx + bodyi.vy * bodyi.vy + bodyi.vz * bodyi.vz ); 136 | 137 | for (let j = i + 1; j < size; j++) { 138 | const bodyj = bodies[j]; 139 | const dx = bodyi.x - bodyj.x; 140 | const dy = bodyi.y - bodyj.y; 141 | const dz = bodyi.z - bodyj.z; 142 | 143 | const distance = Math.sqrt(dx * dx + dy * dy + dz * dz); 144 | e -= (bodyi.mass * bodyj.mass) / distance; 145 | } 146 | } 147 | return e; 148 | } 149 | 150 | const n = +50000000; 151 | 152 | offsetMomentum(); 153 | 154 | console.log(energy().toFixed(9)); 155 | const start = Date.now(); 156 | for (let i = 0; i < n; i++) { 157 | advance(0.01); 158 | } 159 | const end = Date.now(); 160 | console.log(energy().toFixed(9)); 161 | console.log("elapsed:",end-start); 162 | -------------------------------------------------------------------------------- /examples/nodejs/primes.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | function isPrime(p) { 4 | const upper = Math.sqrt(p); 5 | for(let i = 2; i <= upper; i++) { 6 | if (p % i === 0 ) { 7 | return false; 8 | } 9 | } 10 | return true; 11 | } 12 | 13 | // Return n-th prime 14 | function prime(n) { 15 | if (n < 1) { 16 | throw Error("n too small: " + n); 17 | } 18 | let count = 0; 19 | let result = 1; 20 | while(count < n) { 21 | result++; 22 | if (isPrime(result)) { 23 | count++; 24 | } 25 | } 26 | return result; 27 | } 28 | 29 | console.log("your prime is ", prime(100000)); 30 | 31 | }()); 32 | -------------------------------------------------------------------------------- /examples/nodejs/wasm.js: -------------------------------------------------------------------------------- 1 | (function (){ 2 | let bytes = new Uint8Array([ 3 | 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 4 | 0x01, 0x07, 0x01, 0x60, 0x02, 0x7f, 0x7f, 0x01, 5 | 0x7f, 0x03, 0x02, 0x01, 0x00, 0x07, 0x07, 0x01, 6 | 0x03, 0x73, 0x75, 0x6d, 0x00, 0x00, 0x0a, 0x0a, 7 | 0x01, 0x08, 0x00, 0x20, 0x00, 0x20, 0x01, 0x6a, 8 | 0x0f, 0x0b 9 | ]); 10 | 11 | console.log(bytes); 12 | let mod = new WebAssembly.Module(bytes); 13 | let instance = new WebAssembly.Instance(mod, {}); 14 | console.log(instance.exports); 15 | return instance.exports.sum(2020, 1); 16 | }()); 17 | -------------------------------------------------------------------------------- /examples/python3/factorial.py: -------------------------------------------------------------------------------- 1 | def factorial(): 2 | f, n = 1, 1 3 | while True: # First iteration: 4 | yield f # yield 1 to start with and then 5 | f, n = f * n, n+1 # f will now be 1, and n will be 2, ... 6 | 7 | for index, factorial_number in zip(range(51), factorial()): 8 | print('{i:3}!= {f:65}'.format(i=index, f=factorial_number)) 9 | 10 | -------------------------------------------------------------------------------- /examples/python3/fibonacci.py: -------------------------------------------------------------------------------- 1 | def fib(): 2 | a, b = 0, 1 3 | while True: # First iteration: 4 | yield a # yield 0 to start with and then 5 | a, b = b, a + b # a will now be 1, and b will also be 1, (0 + 1) 6 | 7 | for index, fibonacci_number in zip(range(100), fib()): 8 | print('{i:3}: {f:3}'.format(i=index, f=fibonacci_number)) 9 | 10 | -------------------------------------------------------------------------------- /examples/python3/pi.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal, getcontext 2 | getcontext().prec=60 3 | summation = 0 4 | for k in range(50): 5 | summation = summation + 1/Decimal(16)**k * ( 6 | Decimal(4)/(8*k+1) 7 | - Decimal(2)/(8*k+4) 8 | - Decimal(1)/(8*k+5) 9 | - Decimal(1)/(8*k+6) 10 | ) 11 | print(summation) 12 | 13 | -------------------------------------------------------------------------------- /examples/ruby/helloWorld.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | # The famous Hello World 3 | # Program is trivial in 4 | # Ruby. Superfluous: 5 | # 6 | # * A "main" method 7 | # * Newline 8 | # * Semicolons 9 | # 10 | # Here is the Code: 11 | =end 12 | 13 | puts "Hello World!" 14 | 15 | -------------------------------------------------------------------------------- /examples/ruby/love.rb: -------------------------------------------------------------------------------- 1 | # Output "I love Ruby" 2 | say = "I love Ruby"; puts say 3 | 4 | # Output "I *LOVE* RUBY" 5 | say['love'] = "*love*"; puts say.upcase 6 | 7 | # Output "I *love* Ruby", 5 times 8 | 5.times { puts say } 9 | 10 | -------------------------------------------------------------------------------- /examples/ruby/powOf2.rb: -------------------------------------------------------------------------------- 1 | puts(2 ** 1) 2 | puts(2 ** 2) 3 | puts(2 ** 3) 4 | puts(2 ** 10) 5 | puts(2 ** 100) 6 | puts(2 ** 1000) 7 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leaningtech/webvm/e8ead6001d6453b4eeced2b60fddd6c8b85e89e8/favicon.ico -------------------------------------------------------------------------------- /login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Tailscale login 8 | 9 | 10 | Loading network code... 11 | 12 | 13 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes 1; 2 | 3 | events { 4 | worker_connections 1024; 5 | } 6 | 7 | error_log nginx_main_error.log info; 8 | pid nginx_user.pid; 9 | daemon off; 10 | 11 | http { 12 | access_log nginx_access.log; 13 | error_log nginx_error.log info; 14 | 15 | types { 16 | text/html html htm shtml; 17 | text/css css; 18 | application/javascript js; 19 | application/wasm wasm; 20 | image/png png; 21 | image/jpeg jpg jpeg; 22 | image/svg+xml svg; 23 | } 24 | 25 | default_type application/octet-stream; 26 | 27 | sendfile on; 28 | 29 | server { 30 | # listen 8080 ssl; 31 | listen 8081; 32 | server_name localhost; 33 | 34 | gzip on; 35 | # Enable compression for .wasm, .js and .txt files (used for the runtime chunks) 36 | gzip_types application/javascript application/wasm text/plain application/octet-stream; 37 | 38 | charset utf-8; 39 | 40 | # ssl_certificate nginx.crt; 41 | # ssl_certificate_key nginx.key; 42 | 43 | location / { 44 | root build; 45 | autoindex on; 46 | index index.html index.htm; 47 | add_header 'Cross-Origin-Opener-Policy' 'same-origin' always; 48 | add_header 'Cross-Origin-Embedder-Policy' 'require-corp' always; 49 | add_header 'Cross-Origin-Resource-Policy' 'cross-origin' always; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webvm", 3 | "version": "2.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite dev", 7 | "build": "vite build" 8 | }, 9 | "devDependencies": { 10 | "@anthropic-ai/sdk": "^0.33.0", 11 | "@fortawesome/fontawesome-free": "^6.6.0", 12 | "@leaningtech/cheerpx": "latest", 13 | "@oddbird/popover-polyfill": "^0.4.4", 14 | "@sveltejs/adapter-auto": "^3.0.0", 15 | "@sveltejs/adapter-static": "^3.0.5", 16 | "@sveltejs/kit": "^2.0.0", 17 | "@sveltejs/vite-plugin-svelte": "^3.0.0", 18 | "@xterm/addon-fit": "^0.10.0", 19 | "@xterm/addon-web-links": "^0.11.0", 20 | "@xterm/xterm": "^5.5.0", 21 | "autoprefixer": "^10.4.20", 22 | "labs": "git@github.com:leaningtech/labs.git", 23 | "node-html-parser": "^6.1.13", 24 | "postcss": "^8.4.47", 25 | "postcss-discard": "^2.0.0", 26 | "svelte": "^4.2.7", 27 | "tailwindcss": "^3.4.9", 28 | "vite": "^5.0.3", 29 | "vite-plugin-static-copy": "^1.0.6", 30 | "html2canvas-pro": "^1.5.8" 31 | }, 32 | "type": "module" 33 | } 34 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | 'postcss-discard': {rule: function(node, value) 6 | { 7 | if(!value.startsWith('.fa-') || !value.endsWith(":before")) 8 | return false; 9 | switch(value) 10 | { 11 | case '.fa-info-circle:before': 12 | case '.fa-wifi:before': 13 | case '.fa-microchip:before': 14 | case '.fa-compact-disc:before': 15 | case '.fa-discord:before': 16 | case '.fa-github:before': 17 | case '.fa-star:before': 18 | case '.fa-circle:before': 19 | case '.fa-trash-can:before': 20 | case '.fa-book-open:before': 21 | case '.fa-user:before': 22 | case '.fa-screwdriver-wrench:before': 23 | case '.fa-desktop:before': 24 | case '.fa-mouse-pointer:before': 25 | case '.fa-hourglass-half:before': 26 | case '.fa-hand:before': 27 | case '.fa-brain:before': 28 | case '.fa-download:before': 29 | case '.fa-keyboard:before': 30 | case '.fa-thumbtack:before': 31 | case '.fa-brands:before': 32 | case '.fa-solid:before': 33 | case '.fa-regular:before': 34 | return false; 35 | } 36 | return true; 37 | }} 38 | }, 39 | } 40 | -------------------------------------------------------------------------------- /scrollbar.css: -------------------------------------------------------------------------------- 1 | .scrollbar { 2 | scrollbar-color: #777 #0000; 3 | } 4 | 5 | .scrollbar *::-webkit-scrollbar { 6 | height: 6px; 7 | width: 6px; 8 | background-color: #0000; 9 | } 10 | 11 | /* Add a thumb */ 12 | .scrollbar *::-webkit-scrollbar-thumb { 13 | border-radius: 3px; 14 | height: 6px; 15 | width: 6px; 16 | background: #777; 17 | } 18 | 19 | .scrollbar *::-webkit-scrollbar-thumb:hover { 20 | background: #555; 21 | } 22 | -------------------------------------------------------------------------------- /serviceWorker.js: -------------------------------------------------------------------------------- 1 | async function handleFetch(request) { 2 | // Perform the original fetch request and store the result in order to modify the response. 3 | try { 4 | var r = await fetch(request); 5 | } 6 | catch (e) { 7 | console.error(e) 8 | } 9 | if (r.status === 0) { 10 | return r; 11 | } 12 | // We add headers to the original response its headers, in order to enable cross-origin-isolation. And make it independent of the server config. 13 | const newHeaders = new Headers(r.headers); 14 | // COEP & COOP for cross-origin-isolation. 15 | newHeaders.set("Cross-Origin-Embedder-Policy", "require-corp"); 16 | newHeaders.set("Cross-Origin-Opener-Policy", "same-origin"); 17 | newHeaders.set("Cross-Origin-Resource-Policy", "cross-origin"); 18 | /** 19 | * This workaround is necessary due to a limitation of CheerpOS, which relies on the response URL being set to the resolved URL. 20 | * When constructing a new response object, the URL is not set by the Response() constructor and the serviceworker respondwith() method will set the url to event.request.url in case of an empty string. 21 | * To address this, we set the location URL to the resolved response URL and set the status code to 301 in the new Response object. 22 | * This causes the request to bounce back to the serviceworker from Cheerpos, with the event.request.url now set to the resolved URL, which allows the respondWith method to properly set the response URL in our new response. 23 | * https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent/respondWith. 24 | */ 25 | if (r.redirected === true) 26 | newHeaders.set("location", r.url); 27 | // In case of a redirection, we set the status to 301, and body to null, in order to not transfer too much data needlessly 28 | const moddedResponse = new Response(r.redirected === true ? null : r.body, { 29 | headers: newHeaders, 30 | status: r.redirected === true ? 301 : r.status, 31 | statusText: r.statusText, 32 | }); 33 | return moddedResponse; 34 | } 35 | 36 | function serviceWorkerInit() { 37 | // Init the service worker. 38 | self.addEventListener("install", () => self.skipWaiting()); 39 | self.addEventListener("activate", e => e.waitUntil(self.clients.claim())); 40 | // Listen for fetch requests and call handleFetch function. 41 | self.addEventListener("fetch", function (e) { 42 | try { 43 | e.respondWith(handleFetch(e.request)); 44 | } catch (err) { 45 | console.log("Serviceworker NetworkError:" + err); 46 | } 47 | }); 48 | } 49 | 50 | async function doRegister() { 51 | try { 52 | const registration = await navigator.serviceWorker.register(window.document.currentScript.src); 53 | console.log("Service Worker registered", registration.scope); 54 | // EventListener to make sure that the page gets reloaded when a new serviceworker gets installed. 55 | // f.e on first access. 56 | registration.addEventListener("updatefound", () => { 57 | console.log("Reloading the page to transfer control to the Service Worker."); 58 | try { 59 | window.location.reload(); 60 | } catch (err) { 61 | console.log("Service Worker failed reloading the page. ERROR:" + err); 62 | }; 63 | }); 64 | // When the registration is active, but it's not controlling the page, we reload the page to have it take control. 65 | // This f.e occurs when you hard-reload (shift + refresh). https://www.w3.org/TR/service-workers/#navigator-service-worker-controller 66 | if (registration.active && !navigator.serviceWorker.controller) { 67 | console.log("Reloading the page to transfer control to the Service Worker."); 68 | try { 69 | window.location.reload(); 70 | } catch (err) { 71 | console.log("Service Worker failed reloading the page. ERROR:" + err); 72 | }; 73 | } 74 | } 75 | catch { 76 | console.error("Service Worker failed to register:", e) 77 | } 78 | } 79 | 80 | async function serviceWorkerRegister() { 81 | if (window.crossOriginIsolated) return; 82 | if (!window.isSecureContext) { 83 | console.log("Service Worker not registered, a secure context is required."); 84 | return; 85 | } 86 | // Register the service worker and reload the page to transfer control to the serviceworker. 87 | if ("serviceWorker" in navigator) 88 | await doRegister(); 89 | else 90 | console.log("Service worker is not supported in this browser"); 91 | } 92 | 93 | if (typeof window === 'undefined') // If the script is running in a Service Worker context 94 | serviceWorkerInit() 95 | else // If the script is running in the browser context 96 | serviceWorkerRegister(); 97 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | WebVM - Linux virtualization in WebAssembly 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | %sveltekit.head% 34 | 35 | 36 |
%sveltekit.body%
37 | 38 | 39 | -------------------------------------------------------------------------------- /src/lib/AnthropicTab.svelte: -------------------------------------------------------------------------------- 1 | 79 |

Claude AI Integration

80 |

WebVM is integrated with Claude by Anthropic AI. You can prompt the AI to control the system.

81 |

You need to provide your API key. The key is only saved locally to your browser.

82 |
83 |

84 | Conversation history 85 | 86 | 87 | 88 |

89 |
90 |
91 |
92 | {#each $messageList as msg} 93 | {@const details = getMessageDetails(msg)} 94 | {#if details.isToolUse} 95 |

{details.messageContent}

96 | {:else if !details.isToolResult} 97 |

{details.messageContent}

98 | {/if} 99 | {/each} 100 |
101 |
102 |
103 |
104 | {#if $apiState == "KEY_REQUIRED"} 105 |