diff --git a/docs/SECURITY.md b/docs/SECURITY.md index b907d92e27..07a70ee63b 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -48,10 +48,12 @@ We appreciate the community's interest in identifying potential vulnerabilities. 5. **Remediation is required**: Along with the PoC, you must provide **either**: + 1. **A patch/PR**, **or** 2. **a remediation plan** ("actionable steps") that a maintainer can apply without guesswork. Your remediation guidance can include, for example: + - The **likely root cause** (what's wrong and where) - The **location(s)** to change (file/module/function names if known) - The **recommended fix approach** (validation/sanitization rules, auth checks, safe defaults, etc.) diff --git a/src/lib/apis/terminal/index.ts b/src/lib/apis/terminal/index.ts new file mode 100644 index 0000000000..5ffb8f51d9 --- /dev/null +++ b/src/lib/apis/terminal/index.ts @@ -0,0 +1,114 @@ +export type FileEntry = { + name: string; + type: 'file' | 'directory'; + size?: number; + modified?: number; +}; + +export const getCwd = async (baseUrl: string, apiKey: string): Promise => { + const url = `${baseUrl.replace(/\/$/, '')}/files/cwd`; + const res = await fetch(url, { + headers: { Authorization: `Bearer ${apiKey}` } + }).catch(() => null); + if (!res || !res.ok) return null; + const json = await res.json().catch(() => null); + return json?.cwd ?? null; +}; + +export const listFiles = async ( + baseUrl: string, + apiKey: string, + path: string = '/' +): Promise => { + // The endpoint uses `directory` as the query param name + const url = `${baseUrl.replace(/\/$/, '')}/files/list?directory=${encodeURIComponent(path)}`; + const res = await fetch(url, { + headers: { Authorization: `Bearer ${apiKey}` } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error('open-terminal listFiles error:', err); + return null; + }); + return res?.entries ?? null; +}; + +export const readFile = async ( + baseUrl: string, + apiKey: string, + path: string +): Promise => { + const url = `${baseUrl.replace(/\/$/, '')}/files/read?path=${encodeURIComponent(path)}`; + const res = await fetch(url, { + headers: { Authorization: `Bearer ${apiKey}` } + }).catch((err) => { + console.error('open-terminal readFile error:', err); + return null; + }); + + if (!res || !res.ok) return null; + + const contentType = res.headers.get('content-type') ?? ''; + if (contentType.startsWith('image/') || contentType.startsWith('application/octet')) { + // Binary — return a placeholder + return `[Binary file: ${contentType}]`; + } + + // Text files: endpoint returns JSON { path, total_lines, content } + // Binary image files: endpoint returns raw bytes (handled above) + const json = await res.json().catch(() => null); + return json?.content ?? null; +}; + +export const downloadFileBlob = async ( + baseUrl: string, + apiKey: string, + path: string +): Promise<{ blob: Blob; filename: string } | null> => { + const url = `${baseUrl.replace(/\/$/, '')}/files/read?path=${encodeURIComponent(path)}`; + const res = await fetch(url, { + headers: { Authorization: `Bearer ${apiKey}` } + }).catch(() => null); + + if (!res || !res.ok) return null; + + const contentType = res.headers.get('content-type') ?? ''; + const filename = path.split('/').pop() ?? 'file'; + + if (contentType.includes('application/json')) { + const json = await res.json().catch(() => null); + const blob = new Blob([json?.content ?? ''], { type: 'text/plain' }); + return { blob, filename }; + } + + const blob = await res.blob(); + return { blob, filename }; +}; + +export const uploadToTerminal = async ( + baseUrl: string, + apiKey: string, + directory: string, + file: File +): Promise<{ path: string; size: number } | null> => { + const url = `${baseUrl.replace(/\/$/, '')}/files/upload?directory=${encodeURIComponent(directory)}`; + const body = new FormData(); + body.append('file', file); + const res = await fetch(url, { + method: 'POST', + headers: { Authorization: `Bearer ${apiKey}` }, + body + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error('open-terminal uploadToTerminal error:', err); + return null; + }); + return res; +}; diff --git a/src/lib/components/AddTerminalServerModal.svelte b/src/lib/components/AddTerminalServerModal.svelte new file mode 100644 index 0000000000..f42f4896d0 --- /dev/null +++ b/src/lib/components/AddTerminalServerModal.svelte @@ -0,0 +1,297 @@ + + + +
+
+

+ {#if edit} + {$i18n.t('Edit Terminal Connection')} + {:else} + {$i18n.t('Add Terminal Connection')} + {/if} +

+ + +
+ +
+
+
+
+
+
+
+ +
+ +
+ +
+
+
+ +
+
+
+ +
+ +
+ +
+
+
+ + + + {#if showAdvanced} +
+
+
+
+
+ {$i18n.t('OpenAPI Spec')} +
+
+
+ +
+
+
+ + +
+
+
+ +
+ {$i18n.t(`WebUI will make requests to "{{url}}"`, { + url: path.includes('://') + ? path + : `${url}${path.startsWith('/') ? '' : '/'}${path}` + })} +
+
+
+ {/if} + +
+
+
+
+
+ {$i18n.t('Auth')} +
+
+
+ +
+
+ +
+ +
+ {#if auth_type === 'bearer'} + + {:else if auth_type === 'none'} +
+ {$i18n.t('No authentication')} +
+ {:else if auth_type === 'session'} +
+ {$i18n.t('Forwards system user session credentials to authenticate')} +
+ {/if} +
+
+
+
+ +
+
+
+ {#if edit} + + {/if} + + +
+
+
+
+
+
+
+
diff --git a/src/lib/components/AddToolServerModal.svelte b/src/lib/components/AddToolServerModal.svelte index 878fcc5264..aac8f71ee1 100644 --- a/src/lib/components/AddToolServerModal.svelte +++ b/src/lib/components/AddToolServerModal.svelte @@ -542,80 +542,80 @@ {#if showAdvanced} - {#if ['', 'openapi'].includes(type)} -
-
-
-
-
- {$i18n.t('OpenAPI Spec')} + {#if ['', 'openapi'].includes(type)} +
+
+
+
+
+ {$i18n.t('OpenAPI Spec')} +
-
-
-
- -
- -
- {#if spec_type === 'url'} -
- - -
- {:else if spec_type === 'json'} -
+
+