-
Notifications
You must be signed in to change notification settings - Fork 0
feat(workspace): trees + diffs sidepanel via daemon port #46
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -12,6 +12,7 @@ import { DaemonArgosAgentRuntime } from "./host/daemonArgosAgentRuntime"; | |||||||||||||||||||
| import { BunEventPublisher } from "./host/bun-event-publisher"; | ||||||||||||||||||||
| import { initializeDatabase } from "./host/db-init"; | ||||||||||||||||||||
| import { createDaemonDispatcher } from "./dispatch/daemonDispatcher"; | ||||||||||||||||||||
| import { DaemonWorkspacePresenter } from "./workspace/daemonWorkspacePresenter"; | ||||||||||||||||||||
| import { ProviderImportService } from "@argos/backend-core"; | ||||||||||||||||||||
| import { PiProviderExecutionPort } from "./host/pi-provider-execution"; | ||||||||||||||||||||
| import { PiAgentProfileManager } from "./host/piAgentProfileManager"; | ||||||||||||||||||||
|
|
@@ -150,6 +151,14 @@ function withCors(response: Response): Response { | |||||||||||||||||||
| }); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| function inferPreviewContentType(filePath: string): string { | ||||||||||||||||||||
| const ext = filePath.slice(filePath.lastIndexOf(".") + 1).toLowerCase(); | ||||||||||||||||||||
| if (ext === "html" || ext === "htm") return "text/html; charset=utf-8"; | ||||||||||||||||||||
| if (ext === "svg") return "image/svg+xml"; | ||||||||||||||||||||
| if (ext === "pdf") return "application/pdf"; | ||||||||||||||||||||
| return "application/octet-stream"; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| function serveStaticWeb(webRoot: string, pathname: string): Response { | ||||||||||||||||||||
| const safePath = pathname | ||||||||||||||||||||
| .split("/") | ||||||||||||||||||||
|
|
@@ -724,6 +733,8 @@ export async function startDaemon(options?: { | |||||||||||||||||||
| }, | ||||||||||||||||||||
| }); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const workspacePresenter = new DaemonWorkspacePresenter(eventPublisher, "http://127.0.0.1:0"); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const dispatcher = | ||||||||||||||||||||
| options?.dispatcher ?? | ||||||||||||||||||||
| createDaemonDispatcher( | ||||||||||||||||||||
|
|
@@ -743,6 +754,7 @@ export async function startDaemon(options?: { | |||||||||||||||||||
| db, | ||||||||||||||||||||
| environmentId, | ||||||||||||||||||||
| orchestrationRuntime, | ||||||||||||||||||||
| workspacePresenter, | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| setRouteDispatcher(dispatcher); | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -813,6 +825,26 @@ export async function startDaemon(options?: { | |||||||||||||||||||
| return withCors(await handleRouteDispatch(request)); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Workspace file preview (html/pdf/svg) served as raw bytes. The path must | ||||||||||||||||||||
| // resolve inside a registered/allow-listed workspace; otherwise 404. | ||||||||||||||||||||
| if (url.pathname === "/api/v1/workspace/preview" && request.method === "GET") { | ||||||||||||||||||||
| const targetPath = url.searchParams.get("path"); | ||||||||||||||||||||
| if (!targetPath || !workspacePresenter.isPathAllowed(targetPath)) { | ||||||||||||||||||||
| return new Response("Not found", { status: 404 }); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| try { | ||||||||||||||||||||
| const file = Bun.file(targetPath); | ||||||||||||||||||||
| if (!(await file.exists())) return new Response("Not found", { status: 404 }); | ||||||||||||||||||||
| return withCors( | ||||||||||||||||||||
| new Response(file, { | ||||||||||||||||||||
| headers: { "Content-Type": inferPreviewContentType(targetPath), "Cache-Control": "no-store" }, | ||||||||||||||||||||
| }), | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| } catch { | ||||||||||||||||||||
| return new Response("Not found", { status: 404 }); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (url.pathname === "/api/v1/sessions" && request.method === "GET") { | ||||||||||||||||||||
| return withCors(await handleListSessions(sessionAuthRepo)); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
@@ -909,6 +941,7 @@ export async function startDaemon(options?: { | |||||||||||||||||||
| }); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const serverPort = (server as any).port ?? port; | ||||||||||||||||||||
| workspacePresenter.setBaseUrl(`http://${host}:${serverPort}`); | ||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win The preview base URL is wrong for wildcard and IPv6 hosts.
Reuse the same normalization already applied to 🐛 Proposed fix const serverPort = (server as any).port ?? port;
- workspacePresenter.setBaseUrl(`http://${host}:${serverPort}`);
+ const previewHost = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host.includes(":") ? `[${host}]` : host;
+ workspacePresenter.setBaseUrl(`http://${previewHost}:${serverPort}`);
if (!isNonLoopbackHost(host)) {
const originHost = host === "::1" ? "[::1]" : host;
pluginPresenter.setSettingsBaseUrl(`http://${originHost}:${serverPort}`);
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
| if (!isNonLoopbackHost(host)) { | ||||||||||||||||||||
| const originHost = host === "::1" ? "[::1]" : host; | ||||||||||||||||||||
| pluginPresenter.setSettingsBaseUrl(`http://${originHost}:${serverPort}`); | ||||||||||||||||||||
|
|
||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
The preview endpoint serves workspace HTML and SVG as active content on the daemon origin.
inferPreviewContentTypereturnstext/html; charset=utf-8for.htmlandimage/svg+xmlfor.svg. The response comes from the same origin that serves the API and, in web mode, the web UI (Line 790). Any HTML or SVG file inside a registered workspace therefore runs script with access to that origin. A repository under review is untrusted input.Add
X-Content-Type-Options: nosniffand a sandboxing CSP to the preview response.🔒 Proposed fix
return withCors( new Response(file, { - headers: { "Content-Type": inferPreviewContentType(targetPath), "Cache-Control": "no-store" }, + headers: { + "Content-Type": inferPreviewContentType(targetPath), + "Cache-Control": "no-store", + "X-Content-Type-Options": "nosniff", + "Content-Security-Policy": "sandbox; default-src 'none'; img-src data: blob:; style-src 'unsafe-inline'", + }, }), );Also applies to: 154-160
🤖 Prompt for AI Agents