@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,1544 @@
1
+ # SPDX-License-Identifier: FSL-1.1-Apache-2.0
2
+ # Copyright (c) 2025 Open Computer Use Contributors
3
+ """
4
+ File Server for Computer Use Outputs + MCP Endpoint
5
+
6
+ Provides:
7
+ 1. HTTP API for file upload/download
8
+ 2. MCP (Model Context Protocol) endpoint for Computer Use tools
9
+
10
+ See /docs for Swagger UI, /redoc for ReDoc, / for HTML documentation.
11
+ """
12
+
13
+ import os
14
+ import asyncio
15
+ import html as html_module
16
+ import hashlib
17
+ import json
18
+ import mimetypes
19
+ import time
20
+ import zipfile
21
+ from io import BytesIO
22
+ from pathlib import Path
23
+ from typing import Optional, Dict, List, Any
24
+
25
+ import aiohttp
26
+ from fastapi import FastAPI, HTTPException, Header, UploadFile, File, Request, Response, Depends, WebSocket, WebSocketDisconnect, Body
27
+ from fastapi.middleware.cors import CORSMiddleware
28
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
29
+ from fastapi.responses import FileResponse, StreamingResponse, HTMLResponse, PlainTextResponse
30
+ from fastapi.staticfiles import StaticFiles
31
+ from pydantic import BaseModel, Field
32
+
33
+ from system_prompt import SYSTEM_PROMPT_TEMPLATE, build_system_prompt, render_system_prompt
34
+ from docker_manager import (
35
+ get_container_cdp_address,
36
+ PUBLIC_BASE_URL,
37
+ warn_if_public_base_url_is_default,
38
+ warn_if_mcp_api_key_missing,
39
+ warn_subagent_cli,
40
+ )
41
+ from security import sanitize_chat_id, safe_path
42
+ import skill_manager
43
+
44
+
45
+ # =============================================================================
46
+ # MCP Authorization
47
+ # =============================================================================
48
+
49
+ MCP_API_KEY = os.getenv("MCP_API_KEY") # Required for /mcp endpoints
50
+
51
+ security = HTTPBearer(auto_error=False)
52
+
53
+
54
+ async def verify_mcp_auth(credentials: HTTPAuthorizationCredentials = Depends(security)):
55
+ """Verify Bearer token for MCP endpoints."""
56
+ if not MCP_API_KEY:
57
+ # If no key configured, allow all requests (development mode)
58
+ return None
59
+
60
+ if not credentials:
61
+ raise HTTPException(
62
+ status_code=401,
63
+ detail="Missing Authorization header",
64
+ headers={"WWW-Authenticate": "Bearer"}
65
+ )
66
+
67
+ if credentials.credentials != MCP_API_KEY:
68
+ raise HTTPException(
69
+ status_code=401,
70
+ detail="Invalid API key",
71
+ headers={"WWW-Authenticate": "Bearer"}
72
+ )
73
+
74
+ return credentials.credentials
75
+
76
+
77
+ # =============================================================================
78
+ # Pydantic Models for Swagger Documentation
79
+ # =============================================================================
80
+
81
+ class MCPRequest(BaseModel):
82
+ """MCP JSON-RPC Request"""
83
+ jsonrpc: str = Field(default="2.0", description="JSON-RPC version")
84
+ id: int = Field(..., description="Request ID")
85
+ method: str = Field(..., description="MCP method: initialize, tools/list, tools/call")
86
+ params: Dict[str, Any] = Field(default={}, description="Method parameters")
87
+
88
+ model_config = {
89
+ "json_schema_extra": {
90
+ "examples": [
91
+ {
92
+ "jsonrpc": "2.0",
93
+ "id": 1,
94
+ "method": "initialize",
95
+ "params": {
96
+ "protocolVersion": "2024-11-05",
97
+ "capabilities": {},
98
+ "clientInfo": {"name": "test", "version": "1.0"}
99
+ }
100
+ },
101
+ {
102
+ "jsonrpc": "2.0",
103
+ "id": 2,
104
+ "method": "tools/call",
105
+ "params": {
106
+ "name": "bash_tool",
107
+ "arguments": {"command": "echo hello", "description": "test"}
108
+ }
109
+ }
110
+ ]
111
+ }
112
+ }
113
+
114
+
115
+ class MCPResponse(BaseModel):
116
+ """MCP JSON-RPC Response"""
117
+ jsonrpc: str = "2.0"
118
+ id: Optional[int] = None
119
+ result: Optional[Dict[str, Any]] = None
120
+ error: Optional[Dict[str, Any]] = None
121
+
122
+
123
+ class MCPToolInfo(BaseModel):
124
+ """MCP Tool information"""
125
+ name: str
126
+ description: str
127
+
128
+
129
+ class MCPInfo(BaseModel):
130
+ """MCP Server information"""
131
+ name: str
132
+ version: str
133
+ description: str
134
+ tools: List[MCPToolInfo]
135
+ headers: Dict[str, List[str]]
136
+
137
+
138
+ class UploadResponse(BaseModel):
139
+ """File upload response"""
140
+ status: str
141
+ filename: str
142
+ size: int
143
+ md5: str
144
+
145
+
146
+ # =============================================================================
147
+ # FastAPI Application
148
+ # =============================================================================
149
+
150
+ SWAGGER_DESCRIPTION = """
151
+ ## Computer Use Server + MCP
152
+
153
+ HTTP API for file upload/download and **MCP (Model Context Protocol)** endpoint for Computer Use tools.
154
+
155
+ ### Quick Start
156
+
157
+ ```bash
158
+ # Step 1: Initialize (get session ID)
159
+ curl -sD - -X POST "http://localhost:8081/mcp" \\
160
+ -H "Authorization: Bearer <MCP_API_KEY>" \\
161
+ -H "Content-Type: application/json" \\
162
+ -H "Accept: application/json, text/event-stream" \\
163
+ -H "X-Chat-Id: my-session" \\
164
+ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'
165
+ # Response contains header: mcp-session-id: <SESSION_ID>
166
+
167
+ # Step 2: Call a tool
168
+ curl -s -X POST "http://localhost:8081/mcp" \\
169
+ -H "Authorization: Bearer <MCP_API_KEY>" \\
170
+ -H "Content-Type: application/json" \\
171
+ -H "Accept: application/json, text/event-stream" \\
172
+ -H "Mcp-Session-Id: <SESSION_ID>" \\
173
+ -H "X-Chat-Id: my-session" \\
174
+ -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"bash_tool","arguments":{"command":"echo Hello","description":"test"}}}'
175
+ ```
176
+
177
+ ### MCP Tools
178
+
179
+ - **bash_tool** - execute bash commands in isolated Docker container
180
+ - **view** - view files and directories
181
+ - **create_file** - create new files
182
+ - **str_replace** - edit files (text replacement)
183
+ - **sub_agent** - delegate tasks to autonomous agent (Claude Code)
184
+ """
185
+
186
+ from contextlib import asynccontextmanager
187
+
188
+ @asynccontextmanager
189
+ async def lifespan(app):
190
+ """FastAPI lifespan: start MCP session manager for Streamable HTTP.
191
+
192
+ Do NOT swallow ImportError here. The previous version did, and a missing
193
+ `mcp_resources` (e.g. not COPYed in Dockerfile) caused the lifespan to
194
+ yield WITHOUT calling `session_manager.run()`. The MCP ASGI app then
195
+ served every /mcp request with HTTP 500 ("Task group is not
196
+ initialized"), and uvicorn's default error handler hid the traceback —
197
+ a 100% silent failure mode that took hours of bisecting to find.
198
+
199
+ If imports fail, crash loudly so the deploy is obviously broken.
200
+ """
201
+ warn_if_public_base_url_is_default()
202
+ warn_if_mcp_api_key_missing()
203
+ warn_subagent_cli()
204
+ from mcp_tools import mcp as _mcp_server
205
+ # Import-for-side-effect: registers @mcp.resource handlers on the
206
+ # FastMCP singleton. Must happen BEFORE streamable_http_app() so the
207
+ # resources capability is advertised in InitializeResult.
208
+ import mcp_resources # noqa: F401
209
+ if _mcp_server._session_manager is None:
210
+ _mcp_server.streamable_http_app() # triggers lazy init of session_manager
211
+ async with _mcp_server.session_manager.run():
212
+ print("[MCP] session_manager.run() entered — /mcp endpoint is live")
213
+ yield
214
+ print("[MCP] session_manager.run() exiting")
215
+
216
+ app = FastAPI(
217
+ title="Computer Use File Server + MCP",
218
+ description=SWAGGER_DESCRIPTION,
219
+ version="2.0.0",
220
+ docs_url="/docs",
221
+ redoc_url="/redoc",
222
+ lifespan=lifespan,
223
+ openapi_tags=[
224
+ {"name": "MCP", "description": "Model Context Protocol endpoint for Computer Use tools"},
225
+ {"name": "Files", "description": "File upload and download"},
226
+ {"name": "System", "description": "Health check and service information"},
227
+ ]
228
+ )
229
+
230
+ # CORS — needed for preview SPA loaded inside iframe (opaque origin → cross-origin fetch)
231
+ app.add_middleware(
232
+ CORSMiddleware,
233
+ allow_origins=["*"],
234
+ allow_methods=["GET", "POST", "OPTIONS"],
235
+ allow_headers=["*"],
236
+ )
237
+
238
+ # Normalize chat_id in URL paths to lowercase.
239
+ # Docker container names are case-sensitive, but browser URLs may contain
240
+ # uppercase hex in UUIDs (e.g. 384B vs 384b). This prevents 404s.
241
+ @app.middleware("http")
242
+ async def normalize_chat_id_case(request, call_next):
243
+ import re as _re
244
+ path = request.scope.get("path", "")
245
+ # Match UUID-like segments in paths like /files/{chat_id}/... or /terminal/{chat_id}/...
246
+ normalized = _re.sub(
247
+ r'/([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})',
248
+ lambda m: '/' + m.group(1).lower(),
249
+ path
250
+ )
251
+ if normalized != path:
252
+ request.scope["path"] = normalized
253
+ return await call_next(request)
254
+
255
+ # Static files (bundled JS/CSS libraries)
256
+ _static_dir = Path(__file__).parent / "static"
257
+ if _static_dir.is_dir():
258
+ app.mount("/static", StaticFiles(directory=str(_static_dir)), name="static")
259
+
260
+ # Base directory where chat data is stored
261
+ # Mounted from host: /tmp/computer-use-data/{chat_id}/outputs/
262
+ BASE_DATA_DIR = Path(os.getenv("BASE_DATA_DIR", "/data"))
263
+
264
+
265
+ # =============================================================================
266
+ # File Classification for Preview
267
+ # =============================================================================
268
+
269
+ _CODE_EXTENSIONS = {
270
+ '.py', '.js', '.ts', '.jsx', '.tsx', '.css', '.json', '.yaml', '.yml',
271
+ '.toml', '.sh', '.bash', '.sql', '.go', '.rs', '.java', '.c', '.cpp',
272
+ '.h', '.hpp', '.rb', '.php', '.swift', '.kt', '.scala', '.r', '.m',
273
+ '.vue', '.svelte', '.xml', '.dockerfile',
274
+ }
275
+
276
+ _TEXT_EXTENSIONS = {'.txt', '.log', '.ini', '.cfg', '.conf', '.env'}
277
+
278
+ _MARKDOWN_EXTENSIONS = {'.md', '.markdown'}
279
+
280
+ _SPREADSHEET_EXTENSIONS = {'.csv', '.tsv'}
281
+
282
+ _IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.bmp', '.ico'}
283
+
284
+ _AUDIO_EXTENSIONS = {'.mp3', '.wav', '.ogg', '.flac', '.m4a', '.aac', '.wma'}
285
+ _VIDEO_EXTENSIONS = {'.mp4', '.webm', '.mkv', '.mov', '.avi'}
286
+
287
+
288
+ def classify_file(filename: str) -> tuple:
289
+ """Classify file by extension. Returns (type_category, mime_type)."""
290
+ ext = Path(filename).suffix.lower()
291
+ mime, _ = mimetypes.guess_type(filename)
292
+ if mime is None:
293
+ mime = 'application/octet-stream'
294
+
295
+ if ext in ('.html', '.htm'):
296
+ return 'html', mime
297
+ if ext in _IMAGE_EXTENSIONS:
298
+ return 'image', mime
299
+ if ext in _AUDIO_EXTENSIONS:
300
+ return 'audio', mime or 'audio/mpeg'
301
+ if ext in _VIDEO_EXTENSIONS:
302
+ return 'video', mime or 'video/mp4'
303
+ if ext == '.pdf':
304
+ return 'pdf', 'application/pdf'
305
+ if ext in _MARKDOWN_EXTENSIONS:
306
+ return 'markdown', 'text/markdown'
307
+ if ext in _SPREADSHEET_EXTENSIONS:
308
+ return 'spreadsheet', mime
309
+ if ext == '.docx':
310
+ return 'docx', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
311
+ if ext in ('.xlsx', '.xls'):
312
+ return 'xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
313
+ if ext == '.pptx':
314
+ return 'pptx', 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
315
+ if ext == '.drawio':
316
+ return 'drawio', 'application/xml'
317
+ if ext in _CODE_EXTENSIONS:
318
+ return 'code', mime
319
+ if ext in _TEXT_EXTENSIONS:
320
+ return 'text', mime
321
+ # Fallback: check mime type
322
+ if mime.startswith('image/'):
323
+ return 'image', mime
324
+ if mime.startswith('audio/'):
325
+ return 'audio', mime
326
+ if mime.startswith('video/'):
327
+ return 'video', mime
328
+ if mime.startswith('text/'):
329
+ return 'text', mime
330
+ return 'other', mime
331
+
332
+
333
+ def format_size(size_bytes: int) -> str:
334
+ """Format file size for display."""
335
+ if size_bytes < 1024:
336
+ return f"{size_bytes} B"
337
+ if size_bytes < 1024 * 1024:
338
+ return f"{size_bytes / 1024:.1f} KB"
339
+ return f"{size_bytes / (1024 * 1024):.1f} MB"
340
+
341
+
342
+ _FILE_ICONS = {
343
+ 'html': '🌐', 'image': '🖼️', 'code': '📝', 'text': '📄',
344
+ 'pdf': '📕', 'markdown': '📃', 'spreadsheet': '📊',
345
+ 'docx': '📄', 'xlsx': '📊', 'pptx': '📊',
346
+ 'audio': '🎵', 'video': '🎬', 'other': '📎',
347
+ }
348
+
349
+
350
+
351
+ @app.get("/", response_class=HTMLResponse, tags=["System"], include_in_schema=False)
352
+ async def root():
353
+ """Main page with MCP and File API documentation"""
354
+ from docs_html import get_root_html
355
+ return get_root_html()
356
+
357
+ @app.get("/api/uploads/{chat_id}/manifest", tags=["Files"])
358
+ async def get_uploads_manifest(chat_id: str) -> Dict[str, str]:
359
+ """
360
+ Get manifest of uploaded files with their MD5 checksums.
361
+
362
+ Args:
363
+ chat_id: Unique chat identifier
364
+
365
+ Returns:
366
+ Dictionary mapping filename to MD5 checksum
367
+ Example: {"file1.txt": "abc123def456", "doc.pdf": "789xyz"}
368
+ """
369
+ chat_id = sanitize_chat_id(chat_id)
370
+ uploads_dir = safe_path(BASE_DATA_DIR, chat_id, "uploads")
371
+
372
+ # Return empty dict if directory doesn't exist yet
373
+ if not uploads_dir.exists():
374
+ return {}
375
+
376
+ manifest = {}
377
+
378
+ # Scan all files in uploads directory
379
+ for file_path in uploads_dir.rglob("*"):
380
+ if file_path.is_file():
381
+ # Calculate MD5 checksum
382
+ md5_hash = hashlib.md5()
383
+ with open(file_path, "rb") as f:
384
+ for chunk in iter(lambda: f.read(8192), b""):
385
+ md5_hash.update(chunk)
386
+
387
+ # Use relative filename as key
388
+ relative_name = file_path.relative_to(uploads_dir)
389
+ manifest[str(relative_name)] = md5_hash.hexdigest()
390
+
391
+ return manifest
392
+
393
+
394
+ @app.get("/api/uploads/{chat_id}/list", tags=["Files"])
395
+ async def list_uploads(chat_id: str, response: Response):
396
+ """List all files in the uploads directory with metadata."""
397
+ chat_id = sanitize_chat_id(chat_id)
398
+ response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
399
+ uploads_dir = safe_path(BASE_DATA_DIR, chat_id, "uploads")
400
+ if not uploads_dir.exists():
401
+ return {"files": [], "total": 0}
402
+ files = []
403
+ for fp in uploads_dir.rglob("*"):
404
+ if not fp.is_file():
405
+ continue
406
+ rel = fp.relative_to(uploads_dir)
407
+ stat = fp.stat()
408
+ files.append({
409
+ "name": fp.name,
410
+ "path": str(rel),
411
+ "size": stat.st_size,
412
+ "modified": stat.st_mtime,
413
+ "container_path": f"/mnt/user-data/uploads/{rel}",
414
+ })
415
+ files.sort(key=lambda f: f["modified"], reverse=True)
416
+ return {"files": files, "total": len(files)}
417
+
418
+
419
+ @app.post("/api/uploads/{chat_id}/{filename:path}", tags=["Files"], response_model=UploadResponse)
420
+ async def upload_file(chat_id: str, filename: str, file: UploadFile = File(...)):
421
+ """
422
+ Upload a file to chat uploads directory.
423
+
424
+ Args:
425
+ chat_id: Unique chat identifier
426
+ filename: Target filename (can include subdirectories)
427
+ file: File to upload
428
+
429
+ Returns:
430
+ Success message with file info
431
+
432
+ Raises:
433
+ 400: Invalid filename or security violation
434
+ """
435
+ chat_id = sanitize_chat_id(chat_id)
436
+ # Create uploads directory if it doesn't exist
437
+ uploads_dir = safe_path(BASE_DATA_DIR, chat_id, "uploads")
438
+ uploads_dir.mkdir(parents=True, exist_ok=True)
439
+
440
+ # Construct target path with traversal protection
441
+ file_path = safe_path(uploads_dir, filename)
442
+
443
+ # Create parent directories if needed
444
+ file_path.parent.mkdir(parents=True, exist_ok=True)
445
+
446
+ # Save file
447
+ try:
448
+ with open(file_path, "wb") as f:
449
+ content = await file.read()
450
+ f.write(content)
451
+
452
+ # Calculate MD5 for confirmation
453
+ md5_hash = hashlib.md5(content).hexdigest()
454
+
455
+ # Tier 6 — refresh MCP resources for this chat so the new file
456
+ # appears in resources/list without reconnecting. Swallow errors
457
+ # so the upload itself succeeds even if MCP resource sync fails.
458
+ try:
459
+ from mcp_resources import sync_chat_resources
460
+ await sync_chat_resources(chat_id)
461
+ except Exception:
462
+ import traceback
463
+ print(f"[uploads] sync_chat_resources failed for chat {chat_id}:\n{traceback.format_exc()}")
464
+
465
+ return {
466
+ "status": "success",
467
+ "filename": filename,
468
+ "size": len(content),
469
+ "md5": md5_hash
470
+ }
471
+ except Exception as e:
472
+ raise HTTPException(status_code=500, detail=f"Failed to save file: {str(e)}")
473
+
474
+
475
+ @app.get("/files/{chat_id}/archive", tags=["Files"])
476
+ async def download_archive(chat_id: str):
477
+ """
478
+ Download entire outputs directory as a zip archive.
479
+
480
+ Args:
481
+ chat_id: Unique chat identifier
482
+
483
+ Returns:
484
+ StreamingResponse with zip archive
485
+
486
+ Raises:
487
+ 404: Directory not found or empty
488
+ """
489
+ chat_id = sanitize_chat_id(chat_id)
490
+ # Construct outputs directory path
491
+ outputs_dir = safe_path(BASE_DATA_DIR, chat_id, "outputs")
492
+
493
+ # Check if directory exists
494
+ if not outputs_dir.exists():
495
+ raise HTTPException(
496
+ status_code=404,
497
+ detail=f"Outputs directory not found for chat: {chat_id}"
498
+ )
499
+
500
+ if not outputs_dir.is_dir():
501
+ raise HTTPException(
502
+ status_code=400,
503
+ detail="Path is not a directory"
504
+ )
505
+
506
+ # Get all files in directory
507
+ files = list(outputs_dir.rglob("*"))
508
+ files = [f for f in files if f.is_file()]
509
+
510
+ if not files:
511
+ raise HTTPException(
512
+ status_code=404,
513
+ detail="No files found in outputs directory"
514
+ )
515
+
516
+ # Create zip archive in memory
517
+ zip_buffer = BytesIO()
518
+
519
+ with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
520
+ for file_path in files:
521
+ # Add file to zip with relative path
522
+ arcname = file_path.relative_to(outputs_dir)
523
+ zip_file.write(file_path, arcname=str(arcname))
524
+
525
+ # Seek to beginning of buffer
526
+ zip_buffer.seek(0)
527
+
528
+ # Return as streaming response
529
+ return StreamingResponse(
530
+ zip_buffer,
531
+ media_type="application/zip",
532
+ headers={
533
+ "Content-Disposition": f"attachment; filename=chat-{chat_id}-outputs.zip"
534
+ }
535
+ )
536
+
537
+
538
+ @app.get("/files/{chat_id}/{filename:path}", tags=["Files"])
539
+ async def download_file(chat_id: str, filename: str, download: Optional[int] = None):
540
+ """
541
+ Download a specific file from chat outputs directory.
542
+
543
+ Args:
544
+ chat_id: Unique chat identifier
545
+ filename: File name (can include subdirectories)
546
+
547
+ Returns:
548
+ FileResponse with the requested file
549
+
550
+ Raises:
551
+ 404: File not found
552
+ """
553
+ chat_id = sanitize_chat_id(chat_id)
554
+ # Construct full path with traversal protection
555
+ outputs_dir = safe_path(BASE_DATA_DIR, chat_id, "outputs")
556
+ file_path = safe_path(outputs_dir, filename)
557
+
558
+ # Check if file exists
559
+ if not file_path.exists():
560
+ raise HTTPException(
561
+ status_code=404,
562
+ detail=f"File not found: {filename}"
563
+ )
564
+
565
+ if not file_path.is_file():
566
+ raise HTTPException(
567
+ status_code=400,
568
+ detail=f"Path is not a file: {filename}"
569
+ )
570
+
571
+ # Return file
572
+ # ?download=1 → force download (Content-Disposition: attachment)
573
+ # default → serve with real MIME type (browser displays inline)
574
+ if download:
575
+ return FileResponse(
576
+ path=file_path,
577
+ filename=file_path.name,
578
+ media_type="application/octet-stream"
579
+ )
580
+ else:
581
+ mime_type = mimetypes.guess_type(str(file_path))[0] or "application/octet-stream"
582
+ return FileResponse(
583
+ path=file_path,
584
+ filename=file_path.name,
585
+ media_type=mime_type
586
+ )
587
+
588
+
589
+ # =============================================================================
590
+ # Output Files API + Preview
591
+ # =============================================================================
592
+
593
+ @app.get("/api/outputs/{chat_id}", tags=["Files"])
594
+ async def list_outputs(chat_id: str, response: Response):
595
+ """List all files in the outputs directory with metadata."""
596
+ chat_id = sanitize_chat_id(chat_id)
597
+ response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
598
+
599
+ outputs_dir = safe_path(BASE_DATA_DIR, chat_id, "outputs")
600
+
601
+ if not outputs_dir.exists():
602
+ return {"chat_id": chat_id, "files": [], "total": 0, "timestamp": time.time()}
603
+
604
+ files = []
605
+
606
+ for file_path in outputs_dir.rglob("*"):
607
+ if not file_path.is_file():
608
+ continue
609
+ if file_path.name.startswith('.'):
610
+ continue
611
+ relative = file_path.relative_to(outputs_dir)
612
+ file_type, mime = classify_file(str(relative))
613
+ stat = file_path.stat()
614
+ files.append({
615
+ "name": file_path.name,
616
+ "path": str(relative),
617
+ "size": stat.st_size,
618
+ "modified": stat.st_mtime,
619
+ "type": file_type,
620
+ "mime": mime,
621
+ "url": f"/files/{chat_id}/{relative}",
622
+ })
623
+
624
+ files.sort(key=lambda f: f["modified"], reverse=True)
625
+ return {"chat_id": chat_id, "files": files, "total": len(files), "timestamp": time.time()}
626
+
627
+
628
+ # =============================================================================
629
+ # Browser CDP Proxy — live browser viewer
630
+ # =============================================================================
631
+
632
+ @app.get("/browser/{chat_id}/status", tags=["Browser"])
633
+ async def browser_status(chat_id: str, response: Response):
634
+ """Check if browser (Chromium CDP) is running in the chat's container."""
635
+ chat_id = sanitize_chat_id(chat_id)
636
+ response.headers["Cache-Control"] = "no-cache, no-store"
637
+ container_ip = get_container_cdp_address(chat_id)
638
+ if not container_ip:
639
+ return {"active": False, "pages": []}
640
+
641
+ try:
642
+ async with aiohttp.ClientSession() as session:
643
+ async with session.get(
644
+ f"http://{container_ip}:9222/json",
645
+ timeout=aiohttp.ClientTimeout(total=2)
646
+ ) as resp:
647
+ pages = await resp.json()
648
+ return {
649
+ "active": True,
650
+ "pages": [
651
+ {"id": p.get("id", ""), "title": p.get("title", ""), "url": p.get("url", "")}
652
+ for p in pages
653
+ if p.get("type") == "page"
654
+ ]
655
+ }
656
+ except Exception:
657
+ return {"active": False, "pages": []}
658
+
659
+
660
+ @app.get("/browser/{chat_id}/json", tags=["Browser"])
661
+ async def browser_cdp_json(chat_id: str):
662
+ """Proxy CDP /json endpoint from container."""
663
+ chat_id = sanitize_chat_id(chat_id)
664
+ container_ip = get_container_cdp_address(chat_id)
665
+ if not container_ip:
666
+ raise HTTPException(404, "Container not found or not running")
667
+
668
+ try:
669
+ async with aiohttp.ClientSession() as session:
670
+ async with session.get(
671
+ f"http://{container_ip}:9222/json",
672
+ timeout=aiohttp.ClientTimeout(total=3)
673
+ ) as resp:
674
+ return await resp.json()
675
+ except Exception:
676
+ raise HTTPException(503, "Browser not available")
677
+
678
+
679
+ @app.get("/browser/{chat_id}/json/version", tags=["Browser"])
680
+ async def browser_cdp_json_version(chat_id: str):
681
+ """Proxy CDP /json/version endpoint from container."""
682
+ chat_id = sanitize_chat_id(chat_id)
683
+ container_ip = get_container_cdp_address(chat_id)
684
+ if not container_ip:
685
+ raise HTTPException(404, "Container not found or not running")
686
+
687
+ try:
688
+ async with aiohttp.ClientSession() as session:
689
+ async with session.get(
690
+ f"http://{container_ip}:9222/json/version",
691
+ timeout=aiohttp.ClientTimeout(total=3)
692
+ ) as resp:
693
+ return await resp.json()
694
+ except Exception:
695
+ raise HTTPException(503, "Browser not available")
696
+
697
+
698
+ @app.websocket("/browser/{chat_id}/devtools/page/{page_id}")
699
+ async def browser_ws_proxy(websocket: WebSocket, chat_id: str, page_id: str):
700
+ """Bidirectional WebSocket proxy for CDP — connects browser viewer to container's Chromium."""
701
+ chat_id = sanitize_chat_id(chat_id)
702
+ container_ip = get_container_cdp_address(chat_id)
703
+ if not container_ip:
704
+ await websocket.close(code=1008, reason="Container not found")
705
+ return
706
+
707
+ await websocket.accept()
708
+
709
+ backend_url = f"ws://{container_ip}:9222/devtools/page/{page_id}"
710
+
711
+ try:
712
+ async with aiohttp.ClientSession() as session:
713
+ async with session.ws_connect(backend_url) as backend_ws:
714
+ async def forward_client_to_backend():
715
+ try:
716
+ while True:
717
+ data = await websocket.receive_text()
718
+ await backend_ws.send_str(data)
719
+ except WebSocketDisconnect:
720
+ pass
721
+ except Exception:
722
+ pass
723
+ finally:
724
+ await backend_ws.close()
725
+
726
+ async def forward_backend_to_client():
727
+ try:
728
+ async for msg in backend_ws:
729
+ if msg.type == aiohttp.WSMsgType.TEXT:
730
+ await websocket.send_text(msg.data)
731
+ elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR):
732
+ break
733
+ except Exception:
734
+ pass
735
+ finally:
736
+ try:
737
+ await websocket.close()
738
+ except Exception:
739
+ pass
740
+
741
+ done, pending = await asyncio.wait(
742
+ [
743
+ asyncio.create_task(forward_client_to_backend()),
744
+ asyncio.create_task(forward_backend_to_client()),
745
+ ],
746
+ return_when=asyncio.FIRST_COMPLETED,
747
+ )
748
+ for task in pending:
749
+ task.cancel()
750
+ except Exception:
751
+ try:
752
+ await websocket.close(code=1011, reason="Backend connection failed")
753
+ except Exception:
754
+ pass
755
+
756
+
757
+
758
+ # =============================================================================
759
+ # Terminal Proxy — ttyd WebSocket terminal (like Browser CDP proxy)
760
+ # =============================================================================
761
+
762
+ def _get_container_for_terminal(chat_id: str):
763
+ """Get running container for terminal access. Does NOT create containers."""
764
+ import re as _re
765
+ from docker_manager import get_docker_client
766
+ client = get_docker_client()
767
+ sanitized_id = _re.sub(r'[^a-zA-Z0-9_.-]', '-', chat_id)
768
+ container_name = f"owui-chat-{sanitized_id}"
769
+ try:
770
+ c = client.containers.get(container_name)
771
+ c.reload()
772
+ if c.status == "running":
773
+ return c
774
+ except Exception:
775
+ pass
776
+ return None
777
+
778
+
779
+ def _get_container_stopped(chat_id: str):
780
+ """Find a stopped (but not removed) container for this chat."""
781
+ import re as _re
782
+ from docker_manager import get_docker_client
783
+ client = get_docker_client()
784
+ sanitized_id = _re.sub(r'[^a-zA-Z0-9_.-]', '-', chat_id)
785
+ container_name = f"owui-chat-{sanitized_id}"
786
+ try:
787
+ c = client.containers.get(container_name)
788
+ c.reload()
789
+ if c.status in ("exited", "created"):
790
+ return c
791
+ except Exception:
792
+ pass
793
+ return None
794
+
795
+
796
+ @app.get("/terminal/{chat_id}/status", tags=["Terminal"])
797
+ async def terminal_status(chat_id: str, response: Response):
798
+ """Check if ttyd terminal is available in the chat's container."""
799
+ chat_id = sanitize_chat_id(chat_id)
800
+ response.headers["Cache-Control"] = "no-cache, no-store"
801
+ container_ip = get_container_cdp_address(chat_id)
802
+ if not container_ip:
803
+ # Check if container exists but is stopped
804
+ container = _get_container_stopped(chat_id)
805
+ if container:
806
+ return {"active": False, "container_stopped": True}
807
+ # Container removed — check if .meta.json exists for resurrection
808
+ from docker_manager import load_container_meta
809
+ if load_container_meta(chat_id):
810
+ return {"active": False, "meta_exists": True}
811
+ return {"active": False}
812
+ try:
813
+ async with aiohttp.ClientSession() as session:
814
+ async with session.get(
815
+ f"http://{container_ip}:7681/",
816
+ timeout=aiohttp.ClientTimeout(total=2)
817
+ ) as resp:
818
+ return {"active": resp.status == 200}
819
+ except Exception:
820
+ return {"active": False}
821
+
822
+
823
+ class StartTtydRequest(BaseModel):
824
+ dangerous_mode: bool = False
825
+
826
+
827
+ @app.post("/terminal/{chat_id}/start-ttyd", tags=["Terminal"])
828
+ async def start_ttyd(chat_id: str, body: StartTtydRequest = Body(default=StartTtydRequest())):
829
+ """Start ttyd + tmux in the container (lazy start).
830
+
831
+ Uses tmux -A (attach-if-exists) so reconnections work without errors.
832
+ If dangerous_mode=True, sets NO_AUTOSTART=1 so .bashrc skips auto-launch —
833
+ the frontend will inject `claude --dangerously-skip-permissions` after connecting.
834
+ """
835
+ chat_id = sanitize_chat_id(chat_id)
836
+ from docker_manager import _execute_bash
837
+ container = _get_container_for_terminal(chat_id)
838
+ if not container:
839
+ raise HTTPException(404, "Container not running")
840
+ # Check if already running
841
+ check = await asyncio.to_thread(
842
+ _execute_bash, container,
843
+ "pgrep -x ttyd > /dev/null 2>&1 && echo RUNNING || echo STOPPED", 5)
844
+ if "RUNNING" in check.get("output", ""):
845
+ return {"started": False, "already_running": True}
846
+ # Start ttyd + tmux in background (-A = attach if session exists, create if not)
847
+ env_prefix = "NO_AUTOSTART=1 " if body.dangerous_mode else ""
848
+ await asyncio.to_thread(
849
+ _execute_bash, container,
850
+ f"cd /home/assistant && echo 'set -g mouse on' > ~/.tmux.conf && LANG=C.UTF-8 {env_prefix}nohup ttyd -W -p 7681 tmux -u new-session -A -s main bash > /dev/null 2>&1 &", 5)
851
+ # Wait briefly for ttyd to start
852
+ await asyncio.sleep(0.5)
853
+ return {"started": True}
854
+
855
+
856
+ @app.post("/terminal/{chat_id}/stop-ttyd", tags=["Terminal"])
857
+ async def stop_ttyd(chat_id: str):
858
+ """Stop ttyd + tmux in the container so next start is fresh (with .bashrc autostart)."""
859
+ chat_id = sanitize_chat_id(chat_id)
860
+ from docker_manager import _execute_bash
861
+ container = _get_container_for_terminal(chat_id)
862
+ if not container:
863
+ raise HTTPException(404, "Container not running")
864
+ await asyncio.to_thread(
865
+ _execute_bash, container,
866
+ "pkill -x ttyd 2>/dev/null; tmux kill-server 2>/dev/null; true", 5)
867
+ return {"stopped": True}
868
+
869
+
870
+ @app.post("/terminal/{chat_id}/restart-container", tags=["Terminal"])
871
+ async def restart_container(chat_id: str):
872
+ """Restart a stopped container. Handles dead networks after deploy."""
873
+ chat_id = sanitize_chat_id(chat_id)
874
+ container = _get_container_stopped(chat_id)
875
+ if not container:
876
+ raise HTTPException(404, "No stopped container found")
877
+ try:
878
+ from docker_manager import get_docker_client, _get_compose_network_name
879
+ client = get_docker_client()
880
+
881
+ # Disconnect from dead networks (left over after docker-compose down/up)
882
+ container.reload()
883
+ old_nets = list(container.attrs.get("NetworkSettings", {}).get("Networks", {}).keys())
884
+ for net_name in old_nets:
885
+ try:
886
+ net = client.networks.get(net_name)
887
+ net.disconnect(container, force=True)
888
+ except Exception:
889
+ pass # Network already dead — ignore
890
+
891
+ # Connect to current compose network (force refresh — network ID changed after deploy)
892
+ compose_net = _get_compose_network_name(force_refresh=True)
893
+ if compose_net:
894
+ try:
895
+ net = client.networks.get(compose_net)
896
+ net.connect(container)
897
+ except Exception as e:
898
+ print(f"[RESTART] Warning: could not connect to {compose_net}: {e}")
899
+
900
+ await asyncio.to_thread(container.start)
901
+ await asyncio.sleep(2) # Wait for entrypoint
902
+ return {"restarted": True}
903
+ except Exception as e:
904
+ raise HTTPException(500, f"Failed to restart: {e}")
905
+
906
+
907
+ @app.post("/terminal/{chat_id}/resurrect-container", tags=["Terminal"])
908
+ async def resurrect_container(chat_id: str):
909
+ """Recreate a removed container using saved .meta.json and existing host data.
910
+
911
+ Used when container was removed by cron but /data/{chat_id}/.meta.json
912
+ still exists. Restores user identity (email, name, MCP servers) from saved metadata.
913
+ """
914
+ chat_id = sanitize_chat_id(chat_id)
915
+ import re as _re
916
+ from docker_manager import load_container_meta, _create_container, _ensure_gitlab_token
917
+ from context_vars import (
918
+ current_chat_id, current_user_email, current_user_name,
919
+ current_mcp_servers,
920
+ )
921
+
922
+ # Guard: container must not already exist
923
+ existing = _get_container_for_terminal(chat_id) or _get_container_stopped(chat_id)
924
+ if existing:
925
+ raise HTTPException(409, "Container already exists. Use restart-container instead.")
926
+
927
+ meta = load_container_meta(chat_id)
928
+ if not meta:
929
+ raise HTTPException(404, "No .meta.json found for this chat")
930
+
931
+ # Set context vars from saved metadata (non-secret only).
932
+ # Tokens (ANTHROPIC_AUTH_TOKEN, VISION_*) come from computer-use-orchestrator ENV
933
+ # via fallback in _create_container.
934
+ current_chat_id.set(chat_id)
935
+ user_email = meta.get("user_email", "")
936
+ user_name = meta.get("user_name", "")
937
+ mcp_servers = meta.get("mcp_servers", "")
938
+ if user_email:
939
+ current_user_email.set(user_email)
940
+ if user_name:
941
+ current_user_name.set(user_name)
942
+ if mcp_servers:
943
+ current_mcp_servers.set(mcp_servers)
944
+
945
+ # Fetch fresh GitLab token by email (never stored on disk)
946
+ await _ensure_gitlab_token()
947
+
948
+ sanitized_id = _re.sub(r'[^a-zA-Z0-9_.-]', '-', chat_id)
949
+ container_name = f"owui-chat-{sanitized_id}"
950
+
951
+ try:
952
+ print(f"[RESURRECT] Recreating {container_name} for {user_email}")
953
+ await asyncio.to_thread(_create_container, chat_id, container_name)
954
+ await asyncio.sleep(2) # Wait for entrypoint
955
+ return {"resurrected": True, "user_email": user_email}
956
+ except Exception as e:
957
+ raise HTTPException(500, f"Failed to resurrect container: {e}")
958
+
959
+
960
+ @app.get("/terminal/{chat_id}/sessions", tags=["Terminal"])
961
+ async def terminal_sessions(chat_id: str, response: Response):
962
+ """List Claude Code JSONL sessions from the container."""
963
+ chat_id = sanitize_chat_id(chat_id)
964
+ response.headers["Cache-Control"] = "no-cache, no-store"
965
+ from docker_manager import _execute_bash
966
+ container = _get_container_for_terminal(chat_id)
967
+ if not container:
968
+ return {"sessions": []}
969
+ result = await asyncio.to_thread(
970
+ _execute_bash, container,
971
+ r"""for f in $(ls -t /home/assistant/.claude/projects/-home-assistant/*.jsonl /root/.claude/projects/-home-assistant/*.jsonl 2>/dev/null | sort -u | head -20); do
972
+ sid=$(basename "$f" .jsonl)
973
+ model=$(grep -o '"model":"[^"]*"' "$f" 2>/dev/null | head -1 | cut -d'"' -f4)
974
+ ts=$(stat -c %Y "$f" 2>/dev/null || stat -f %m "$f" 2>/dev/null)
975
+ label=$(grep -m1 '"type":"user"' "$f" 2>/dev/null | sed 's/.*"content":\s*"//' | sed 's/".*//' | cut -c1-80)
976
+ echo "$sid|$model|$ts|$label"
977
+ done""", 10)
978
+ sessions = []
979
+ for line in (result.get("output", "") or "").strip().split("\n"):
980
+ if not line.strip() or "|" not in line:
981
+ continue
982
+ parts = line.strip().split("|", 3)
983
+ if len(parts) >= 3:
984
+ sessions.append({
985
+ "session_id": parts[0],
986
+ "model": parts[1] or "unknown",
987
+ "timestamp": float(parts[2]) if parts[2] else 0,
988
+ "label": parts[3].strip() if len(parts) > 3 else "",
989
+ })
990
+ return {"sessions": sessions}
991
+
992
+
993
+ @app.get("/terminal/{chat_id}/processes", tags=["Terminal"])
994
+ async def terminal_processes(chat_id: str, response: Response):
995
+ """List running Claude processes in the container."""
996
+ chat_id = sanitize_chat_id(chat_id)
997
+ response.headers["Cache-Control"] = "no-cache, no-store"
998
+ from docker_manager import _execute_bash
999
+ container = _get_container_for_terminal(chat_id)
1000
+ if not container:
1001
+ return {"processes": []}
1002
+ result = await asyncio.to_thread(
1003
+ _execute_bash, container,
1004
+ r"ps -eo pid,etimes,args --no-headers 2>/dev/null | grep '[c]laude-code/cli.js' | head -10", 10)
1005
+ processes = []
1006
+ for line in (result.get("output", "") or "").strip().split("\n"):
1007
+ line = line.strip()
1008
+ if not line:
1009
+ continue
1010
+ parts = line.split(None, 2)
1011
+ if len(parts) >= 3:
1012
+ try:
1013
+ pid = int(parts[0])
1014
+ elapsed_sec = int(parts[1])
1015
+ command = parts[2]
1016
+ processes.append({
1017
+ "pid": pid,
1018
+ "elapsed_minutes": elapsed_sec // 60,
1019
+ "command": command[:100], # truncate for display
1020
+ })
1021
+ except (ValueError, IndexError):
1022
+ continue
1023
+ return {"processes": processes}
1024
+
1025
+
1026
+ @app.post("/terminal/{chat_id}/processes/{pid}/kill", tags=["Terminal"])
1027
+ async def kill_terminal_process(chat_id: str, pid: int):
1028
+ """Kill a Claude process in the container by PID."""
1029
+ chat_id = sanitize_chat_id(chat_id)
1030
+ container = _get_container_for_terminal(chat_id)
1031
+ if not container:
1032
+ raise HTTPException(404, "Container not running")
1033
+ # Validate: only kill claude processes (not PID 1 or random processes)
1034
+ def _do_kill():
1035
+ check = container.exec_run(
1036
+ ["bash", "-c", f"ps -p {pid} -o args= 2>/dev/null | grep -q claude && echo OK || echo DENIED"],
1037
+ demux=True)
1038
+ stdout = (check.output[0] or b"").decode().strip() if check.output else ""
1039
+ if "OK" not in stdout:
1040
+ return False
1041
+ # SIGTERM first, then SIGKILL if still alive after 2s
1042
+ container.exec_run(["kill", "-15", str(pid)])
1043
+ import time; time.sleep(2)
1044
+ alive = container.exec_run(
1045
+ ["bash", "-c", f"kill -0 {pid} 2>/dev/null && echo ALIVE || echo DEAD"],
1046
+ demux=True)
1047
+ alive_out = (alive.output[0] or b"").decode().strip() if alive.output else ""
1048
+ if "ALIVE" in alive_out:
1049
+ container.exec_run(["kill", "-9", str(pid)])
1050
+ return True
1051
+ killed = await asyncio.to_thread(_do_kill)
1052
+ if not killed:
1053
+ raise HTTPException(400, "Can only kill Claude processes")
1054
+ return {"killed": True, "pid": pid}
1055
+
1056
+
1057
+ @app.get("/terminal/{chat_id}/heartbeat", tags=["Terminal"])
1058
+ async def terminal_heartbeat(chat_id: str):
1059
+ """Reset container idle timer. Called by JS every 2 min while page is open."""
1060
+ chat_id = sanitize_chat_id(chat_id)
1061
+ container = _get_container_for_terminal(chat_id)
1062
+ if container:
1063
+ from docker_manager import _reset_shutdown_timer
1064
+ await asyncio.to_thread(_reset_shutdown_timer, container)
1065
+ return {"ok": True}
1066
+
1067
+
1068
+ @app.websocket("/terminal/{chat_id}/ws")
1069
+ async def terminal_ws_proxy(websocket: WebSocket, chat_id: str):
1070
+ """Bidirectional WebSocket proxy — connects xterm.js to container's ttyd."""
1071
+ chat_id = sanitize_chat_id(chat_id)
1072
+ container_ip = get_container_cdp_address(chat_id)
1073
+ if not container_ip:
1074
+ await websocket.close(code=1008, reason="Container not found")
1075
+ return
1076
+
1077
+ await websocket.accept(subprotocol="tty")
1078
+
1079
+ backend_url = f"ws://{container_ip}:7681/ws"
1080
+
1081
+ try:
1082
+ async with aiohttp.ClientSession() as session:
1083
+ async with session.ws_connect(backend_url, protocols=["tty"]) as backend_ws:
1084
+ async def forward_client_to_backend():
1085
+ try:
1086
+ while True:
1087
+ msg = await websocket.receive()
1088
+ if msg["type"] == "websocket.disconnect":
1089
+ break
1090
+ if msg.get("bytes"):
1091
+ await backend_ws.send_bytes(msg["bytes"])
1092
+ elif msg.get("text"):
1093
+ await backend_ws.send_str(msg["text"])
1094
+ except WebSocketDisconnect:
1095
+ pass
1096
+ except Exception:
1097
+ pass
1098
+ finally:
1099
+ await backend_ws.close()
1100
+
1101
+ async def forward_backend_to_client():
1102
+ try:
1103
+ async for msg in backend_ws:
1104
+ if msg.type == aiohttp.WSMsgType.BINARY:
1105
+ await websocket.send_bytes(msg.data)
1106
+ elif msg.type == aiohttp.WSMsgType.TEXT:
1107
+ await websocket.send_text(msg.data)
1108
+ elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR):
1109
+ break
1110
+ except Exception:
1111
+ pass
1112
+ finally:
1113
+ try:
1114
+ await websocket.close()
1115
+ except Exception:
1116
+ pass
1117
+
1118
+ done, pending = await asyncio.wait(
1119
+ [
1120
+ asyncio.create_task(forward_client_to_backend()),
1121
+ asyncio.create_task(forward_backend_to_client()),
1122
+ ],
1123
+ return_when=asyncio.FIRST_COMPLETED,
1124
+ )
1125
+ for task in pending:
1126
+ task.cancel()
1127
+ except Exception:
1128
+ try:
1129
+ await websocket.close(code=1011, reason="Backend connection failed")
1130
+ except Exception:
1131
+ pass
1132
+
1133
+
1134
+ @app.get("/preview/{chat_id}", response_class=HTMLResponse, tags=["Files"])
1135
+ async def preview_page(chat_id: str, response: Response):
1136
+ """
1137
+ Self-contained file preview SPA.
1138
+ Shows output files with auto-refresh, file navigation, and type-specific preview.
1139
+ Designed to be embedded in an iframe (Open WebUI Artifacts panel).
1140
+ """
1141
+ chat_id = sanitize_chat_id(chat_id)
1142
+ response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
1143
+ # Use relative URLs so it works behind HTTPS reverse proxy
1144
+ api_url = f"/api/outputs/{chat_id}"
1145
+ files_base = f"/files/{chat_id}"
1146
+
1147
+ return _generate_preview_html(chat_id, api_url, files_base)
1148
+
1149
+
1150
+ def _generate_preview_html(chat_id: str, api_url: str, files_base: str) -> str:
1151
+ """Generate the preview SPA HTML page."""
1152
+ return f'''<!DOCTYPE html>
1153
+ <html lang="en">
1154
+ <head>
1155
+ <meta charset="UTF-8">
1156
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1157
+ <title>File Preview</title>
1158
+ <link rel="stylesheet" href="/static/preview.css">
1159
+ <link rel="stylesheet" href="/static/github.min.css" media="(prefers-color-scheme: light)">
1160
+ <link rel="stylesheet" href="/static/github-dark.min.css" media="(prefers-color-scheme: dark)">
1161
+ <link rel="stylesheet" href="/static/katex/katex.min.css">
1162
+ <link rel="stylesheet" href="/static/xterm.css">
1163
+ <script src="/static/highlight.min.js"></script>
1164
+ <script src="/static/highlightjs-line-numbers.min.js"></script>
1165
+ <script src="/static/marked.min.js"></script>
1166
+ <script src="/static/xterm.min.js"></script>
1167
+ <script src="/static/xterm-addon-fit.min.js"></script>
1168
+ <script src="/static/xterm-addon-web-links.min.js"></script>
1169
+ </head>
1170
+ <body>
1171
+ <div id="app"></div>
1172
+ <script>
1173
+ window.__CONFIG__ = {{
1174
+ apiUrl: {json.dumps(api_url)},
1175
+ filesBase: {json.dumps(files_base)},
1176
+ chatId: {json.dumps(chat_id)}
1177
+ }};
1178
+ // Heartbeat: keep container alive while page is open (every 2 min)
1179
+ setInterval(function() {{ fetch('/terminal/' + {json.dumps(chat_id)} + '/heartbeat').catch(function(){{}}); }}, 120000);
1180
+ </script>
1181
+ <script type="module" src="/static/preview.js"></script>
1182
+ </body>
1183
+ </html>'''
1184
+
1185
+
1186
+ @app.get("/system-prompt", response_class=PlainTextResponse, tags=["System"])
1187
+ async def system_prompt(
1188
+ request: Request,
1189
+ chat_id: Optional[str] = None,
1190
+ file_base_url: Optional[str] = None,
1191
+ archive_url: Optional[str] = None,
1192
+ user_email: Optional[str] = None,
1193
+ ):
1194
+ """
1195
+ Get Computer Use system prompt for AI integrations.
1196
+
1197
+ Tier 6 — backward-compat HTTP endpoint. Open WebUI filter fetches this URL
1198
+ and injects the response into the LLM's system message. Every other
1199
+ consumer (Agents SDK, Inspector, Claude Desktop) should prefer the native
1200
+ MCP tiers (InitializeResult.instructions on initialize, or just
1201
+ /home/assistant/README.md inside the sandbox).
1202
+
1203
+ chat_id resolution, in priority order:
1204
+ 1. request header (X-Chat-Id / X-User-Email, plus X-OpenWebUI-* aliases
1205
+ to match the rest of the server — see mcp_tools.set_context_from_headers)
1206
+ 2. `chat_id` query param
1207
+ 3. last path segment of the deprecated `file_base_url` query param
1208
+ (kept for backwards compat with pre-v4.0.0 integrations; see below)
1209
+ 4. "default"
1210
+
1211
+ `file_base_url` / `archive_url` are deprecated query params from pre-v4.0.0:
1212
+ before the server owned `PUBLIC_BASE_URL`, clients passed their own
1213
+ browser-facing URLs here. Now the server always emits PUBLIC_BASE_URL/files/
1214
+ {chat_id} and we only still read `file_base_url` to extract the trailing
1215
+ chat_id for legacy callers. `archive_url` is ignored entirely — the server
1216
+ derives archive URLs from PUBLIC_BASE_URL + chat_id.
1217
+ """
1218
+ def _header(*names: str) -> Optional[str]:
1219
+ for n in names:
1220
+ v = request.headers.get(n)
1221
+ if v:
1222
+ return v
1223
+ return None
1224
+
1225
+ def _extract_chat_id_from_legacy_url(url: Optional[str]) -> Optional[str]:
1226
+ if not url:
1227
+ return None
1228
+ tail = url.rstrip("/").rsplit("/", 1)[-1]
1229
+ return tail or None
1230
+
1231
+ raw_chat_id = (
1232
+ _header("x-chat-id", "x-openwebui-chat-id")
1233
+ or chat_id
1234
+ or _extract_chat_id_from_legacy_url(file_base_url)
1235
+ )
1236
+ # Sanitize at the boundary — same invariant other endpoints in this
1237
+ # file enforce (see /files/{chat_id}/archive). Without this,
1238
+ # untrusted chat_id from headers/query lands in /system-prompt URLs
1239
+ # via the renderer.
1240
+ effective_chat_id = sanitize_chat_id(raw_chat_id) if raw_chat_id else None
1241
+ effective_user_email = (
1242
+ _header("x-user-email", "x-openwebui-user-email") or user_email
1243
+ )
1244
+
1245
+ # Single rendering path — shared cache with Tiers 2, 4, 5.
1246
+ # chat_id=None preserves the legacy diagnostic path: external callers
1247
+ # (n8n et al.) hitting /system-prompt with no params get the template
1248
+ # back with placeholders intact and substitute them themselves.
1249
+ result = await render_system_prompt(
1250
+ effective_chat_id,
1251
+ effective_user_email,
1252
+ )
1253
+
1254
+ # Public URL is owned by the server. Expose via response header so the
1255
+ # Open WebUI filter can decorate outlet() with browser-facing links without
1256
+ # needing its own Valve — its ORCHESTRATOR_URL is internal-only.
1257
+ return PlainTextResponse(
1258
+ content=result,
1259
+ headers={"X-Public-Base-URL": PUBLIC_BASE_URL},
1260
+ )
1261
+
1262
+
1263
+ @app.get("/skill-mounts", tags=["System"])
1264
+ async def skill_mounts_endpoint(user_email: Optional[str] = None):
1265
+ """
1266
+ Get Docker volume mounts for user-uploaded skills.
1267
+
1268
+ Returns dict of {host_path: {"bind": container_path, "mode": "ro"}}
1269
+ for use by computer_use_tools.py when creating containers.
1270
+ """
1271
+ if not user_email:
1272
+ return {}
1273
+ skills = await skill_manager.get_user_skills(user_email)
1274
+ for s in skills:
1275
+ if s.category == "user":
1276
+ await skill_manager.ensure_skill_cached(s)
1277
+ return skill_manager.get_skill_mounts(skills)
1278
+
1279
+
1280
+ @app.get("/skill-list", response_class=PlainTextResponse, tags=["System"])
1281
+ async def skill_list_endpoint(
1282
+ user_email: Optional[str] = None,
1283
+ format: str = "sub_agent",
1284
+ ):
1285
+ """
1286
+ Get skills list as text for sub-agent prompt.
1287
+
1288
+ Returns formatted text: "- name: location - description" per line.
1289
+ """
1290
+ skills = await skill_manager.get_user_skills(user_email)
1291
+ if format == "sub_agent":
1292
+ return skill_manager.build_sub_agent_skills_text(skills)
1293
+ return skill_manager.build_sub_agent_skills_text(skills)
1294
+
1295
+
1296
+ @app.get("/health", tags=["System"])
1297
+ async def health():
1298
+ """Health check for monitoring"""
1299
+ return {"status": "healthy"}
1300
+
1301
+
1302
+ # ============================================================================
1303
+ # Skill Usage Stats
1304
+ # ============================================================================
1305
+
1306
+ CENTRAL_LOG = BASE_DATA_DIR / "skill-usage-central.jsonl"
1307
+ _central_log_lock = asyncio.Lock()
1308
+
1309
+
1310
+ def _harvest_and_get_stats() -> dict:
1311
+ """
1312
+ Scan per-chat .skill-usage.jsonl files, append new events to central log,
1313
+ then aggregate stats from the central log.
1314
+ """
1315
+ seen: set = set()
1316
+
1317
+ # Load already-seen events from central log (dedup key: ts+chat_id+skill)
1318
+ if CENTRAL_LOG.exists():
1319
+ with open(CENTRAL_LOG) as f:
1320
+ for line in f:
1321
+ try:
1322
+ ev = json.loads(line)
1323
+ seen.add((ev["ts"], ev.get("chat_id", ""), ev.get("skill", "")))
1324
+ except Exception:
1325
+ pass
1326
+
1327
+ # Harvest new events from per-chat logs
1328
+ new_events: list = []
1329
+ for log_path in BASE_DATA_DIR.glob("*/outputs/.skill-usage.jsonl"):
1330
+ try:
1331
+ with open(log_path) as f:
1332
+ for line in f:
1333
+ try:
1334
+ ev = json.loads(line.strip())
1335
+ key = (ev["ts"], ev.get("chat_id", ""), ev.get("skill", ""))
1336
+ if key not in seen:
1337
+ seen.add(key)
1338
+ new_events.append(ev)
1339
+ except Exception:
1340
+ pass
1341
+ except OSError:
1342
+ pass
1343
+
1344
+ # Persist new events to central log
1345
+ if new_events:
1346
+ with open(CENTRAL_LOG, "a") as f:
1347
+ for ev in new_events:
1348
+ f.write(json.dumps(ev, ensure_ascii=False) + "\n")
1349
+
1350
+ # Aggregate stats from central log
1351
+ stats: dict = {}
1352
+ if CENTRAL_LOG.exists():
1353
+ with open(CENTRAL_LOG) as f:
1354
+ for line in f:
1355
+ try:
1356
+ ev = json.loads(line)
1357
+ skill = ev.get("skill", "unknown")
1358
+ email = ev.get("email", "unknown")
1359
+ s = stats.setdefault(skill, {"total": 0, "by_email": {}})
1360
+ s["total"] += 1
1361
+ s["by_email"][email] = s["by_email"].get(email, 0) + 1
1362
+ except Exception:
1363
+ pass
1364
+
1365
+ return {
1366
+ "skills": stats,
1367
+ "total_events": sum(s["total"] for s in stats.values()),
1368
+ "harvested_new": len(new_events),
1369
+ }
1370
+
1371
+
1372
+ @app.get("/api/skill-stats", tags=["System"])
1373
+ async def skill_stats(x_internal_api_key: Optional[str] = Header(default=None)):
1374
+ """
1375
+ Aggregate skill usage stats from all chat containers.
1376
+
1377
+ Scans per-chat .skill-usage.jsonl files written by the inotify watcher
1378
+ running inside containers, persists new events to a central durable log,
1379
+ and returns aggregated counts per skill and user email.
1380
+
1381
+ Requires X-Internal-Api-Key header (matches MCP_TOKENS_API_KEY env var).
1382
+ """
1383
+ api_key = os.getenv("MCP_TOKENS_API_KEY", "")
1384
+ if api_key and x_internal_api_key != api_key:
1385
+ raise HTTPException(status_code=403, detail="Forbidden")
1386
+ async with _central_log_lock:
1387
+ result = await asyncio.get_event_loop().run_in_executor(None, _harvest_and_get_stats)
1388
+ return result
1389
+
1390
+
1391
+ # ============================================================================
1392
+ # Runtime introspection (Phase 9.5 — Preview SPA multi-CLI surface)
1393
+ # ============================================================================
1394
+
1395
+ @app.get("/api/runtime/cli", tags=["System"])
1396
+ async def runtime_cli(response: Response):
1397
+ """Return the active sub-agent CLI runtime.
1398
+
1399
+ Pure additive endpoint — no auth, no side effects, no breakage of the
1400
+ existing MCP contract. Surfaces what `SUBAGENT_CLI` resolved to at
1401
+ orchestrator boot (`docker_manager.SUBAGENT_CLI`) plus the per-CLI
1402
+ default model so the Preview SPA can render an active-CLI badge.
1403
+
1404
+ Cache-Control: no-store — the value is fixed for the orchestrator
1405
+ lifetime, but operators flipping `SUBAGENT_CLI` in `.env` and
1406
+ restarting must see the new value immediately on next page load.
1407
+ """
1408
+ response.headers["Cache-Control"] = "no-store"
1409
+ # Resolve through cli_runtime so the badge reflects the SAME default
1410
+ # the dispatcher will actually run, including all env-var overrides
1411
+ # (CODEX_SUB_AGENT_DEFAULT_MODEL, OPENCODE_MODEL, etc.). Calling
1412
+ # resolve_subagent_model("", cli) returns the (model_id, display_name)
1413
+ # tuple — we surface model_id since that's what hits the wire.
1414
+ # CR PR#76 finding #1: do not duplicate the per-CLI fallback table here.
1415
+ from cli_runtime import Cli, resolve_cli, resolve_subagent_model
1416
+ cli = resolve_cli()
1417
+ # Phase 2: opencode/codex no longer have a hardcoded fallback. When the
1418
+ # operator hasn't set a per-CLI default env, resolve_subagent_model raises
1419
+ # a ValueError pointing them at list-subagent-models. Surface that as
1420
+ # default_model=null so the badge can render "no default — set
1421
+ # <CLI>_SUB_AGENT_DEFAULT_MODEL" instead of crashing the endpoint.
1422
+ try:
1423
+ model_id, _display = resolve_subagent_model("", cli)
1424
+ except ValueError:
1425
+ model_id = None
1426
+ return {
1427
+ "cli": cli.value,
1428
+ "default_model": model_id,
1429
+ "supports_cost": cli == Cli.CLAUDE,
1430
+ }
1431
+
1432
+
1433
+ # ============================================================================
1434
+ # MCP Endpoint Integration
1435
+ # ============================================================================
1436
+
1437
+ # MCP requires initialization via lifespan context manager
1438
+ # We'll use a custom SSE-based approach for better compatibility
1439
+
1440
+ _mcp_server = None
1441
+ _mcp_set_context = None
1442
+
1443
+
1444
+ def _init_mcp():
1445
+ """Initialize MCP server (lazy load).
1446
+
1447
+ Note: ImportError used to be caught here. After the lifespan rewrite that
1448
+ crashes loud on missing mcp_tools, that catch is dead code — if the
1449
+ import would fail, the server never gets past startup. Keeping the
1450
+ import unguarded so any stale assumption blows up at the call site
1451
+ rather than silently returning None.
1452
+ """
1453
+ global _mcp_server, _mcp_set_context
1454
+ if _mcp_server is None:
1455
+ from mcp_tools import mcp, set_context_from_headers
1456
+ _mcp_server = mcp
1457
+ _mcp_set_context = set_context_from_headers
1458
+ return _mcp_server, _mcp_set_context
1459
+
1460
+
1461
+ # =============================================================================
1462
+ # Mount native MCP Streamable HTTP app (SSE + progress notifications)
1463
+ # =============================================================================
1464
+ # Replaces the old custom JSON-RPC handler with FastMCP's native Starlette app.
1465
+ # This enables:
1466
+ # - SSE streaming for real-time progress updates via ctx.report_progress()
1467
+ # - Standard MCP protocol compliance (initialize, tools/list, tools/call)
1468
+ # - Session management by FastMCP
1469
+ #
1470
+ # Example usage with MCP client:
1471
+ # from mcp import ClientSession
1472
+ # from mcp.client.streamable_http import streamablehttp_client
1473
+ #
1474
+ # async with streamablehttp_client(
1475
+ # "http://localhost:8081/mcp",
1476
+ # headers={"X-Chat-Id": "my-chat-123"}
1477
+ # ) as (read, write, _):
1478
+ # async with ClientSession(read, write) as session:
1479
+ # await session.initialize()
1480
+ # tools = await session.list_tools()
1481
+ # result = await session.call_tool("bash_tool", {
1482
+ # "command": "echo hello",
1483
+ # "description": "Test command"
1484
+ # })
1485
+
1486
+ # Module-level MCP route registration — runs at import time, before lifespan.
1487
+ # ImportError used to be caught here too; removed because the lifespan now
1488
+ # also imports mcp_tools and crashes loud, so a missing module produced two
1489
+ # different failure modes for the same root cause (silent skip here, then
1490
+ # crash from lifespan). Single failure mode is easier to debug.
1491
+ from mcp_tools import get_mcp_app
1492
+ from starlette.routing import Route
1493
+
1494
+ _mcp_asgi_app = get_mcp_app(api_key=MCP_API_KEY)
1495
+
1496
+
1497
+ async def _mcp_endpoint(request):
1498
+ """Forward to MCP ASGI app."""
1499
+ # Rewrite path to "/" (root of MCP app)
1500
+ scope = dict(request.scope)
1501
+ scope["path"] = "/"
1502
+ scope["raw_path"] = b"/"
1503
+ await _mcp_asgi_app(scope, request.receive, request._send)
1504
+
1505
+
1506
+ app.routes.insert(0, Route("/mcp", endpoint=_mcp_endpoint, methods=["POST", "GET", "DELETE"]))
1507
+ print("[MCP] Native Streamable HTTP MCP app route added at /mcp")
1508
+
1509
+
1510
+ @app.get("/mcp-info", tags=["MCP"], response_model=MCPInfo)
1511
+ async def mcp_info(_auth: str = Depends(verify_mcp_auth)):
1512
+ """Get MCP endpoint information (for documentation/debugging)."""
1513
+ mcp_server, _ = _init_mcp()
1514
+
1515
+ if mcp_server is None:
1516
+ raise HTTPException(
1517
+ status_code=503,
1518
+ detail="MCP endpoint not available"
1519
+ )
1520
+
1521
+ tools = []
1522
+ for tool in mcp_server._tool_manager.list_tools():
1523
+ tools.append(MCPToolInfo(
1524
+ name=tool.name,
1525
+ description=tool.description.strip() if tool.description else ""
1526
+ ))
1527
+
1528
+ return MCPInfo(
1529
+ name="computer-use-mcp",
1530
+ version="1.0.0",
1531
+ description="Computer Use tools via MCP - command execution in isolated Docker containers",
1532
+ tools=tools,
1533
+ headers={
1534
+ "required": ["X-Chat-Id"],
1535
+ "optional": [
1536
+ "X-User-Email",
1537
+ "X-User-Name",
1538
+ "X-Gitlab-Token",
1539
+ "X-Gitlab-Host",
1540
+ "X-Anthropic-Api-Key",
1541
+ "X-Anthropic-Base-Url"
1542
+ ]
1543
+ }
1544
+ )