Skip to content

Commit edbbcf0

Browse files
authored
feat: add direct sandbox workspace preview (#7316)
* feat: add direct sandbox workspace preview * refactor: align sandbox preview claims with upstream refactor: fix IDE agent internal ports fix: decode sandbox proxy claims generically ci: install Rust toolchain without third-party actions * chore: keep sandbox proxy on one port by default feat: split sandbox proxy preview port fix: require sandbox preview proxy URL
1 parent 53c5598 commit edbbcf0

40 files changed

Lines changed: 2411 additions & 322 deletions

File tree

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# Agent Sandbox Workspace Direct Preview
2+
3+
## 1. Goal
4+
5+
Replace sandbox file S3 export with a short-lived, read-only URL:
6+
7+
```text
8+
https://agent-proxy.fastgpt.cn/preview/{token}/test/preview.html
9+
```
10+
11+
The token authenticates one sandbox workspace. The remaining URL is resolved as a workspace-relative
12+
path. File bytes flow from the sandbox IDE agent through `agent-sandbox-proxy`; FastGPT only authenticates
13+
the business session, signs the token, and resolves the provider endpoint.
14+
15+
## 2. Scope
16+
17+
- Replace HTML S3 preview with direct workspace preview.
18+
- Replace `sandbox_get_file_url` S3 uploads with direct workspace URLs.
19+
- Support `GET`, `HEAD`, content type, content length, ETag, and single byte ranges.
20+
- Preserve relative HTML references such as `./assets/app.js` and `../images/a.png`.
21+
- Keep existing `fs` and `terminal` WebSocket behavior unchanged.
22+
- Support provider endpoint resolution through `ISandbox.getEndpoint`.
23+
24+
The existing S3 workspace archive lifecycle is unrelated and remains unchanged.
25+
26+
## 3. Architecture
27+
28+
```text
29+
Browser / model
30+
-> GET /preview/{token}/{workspacePath}
31+
-> agent-sandbox-proxy
32+
1. verifies HMAC token locally
33+
2. resolves the sandbox endpoint through FastGPT verifyTicket using an internal header
34+
3. proxies an authenticated HTTP request to port 1319
35+
-> fastgpt-ide-agent preview listener
36+
1. validates the internal agent password
37+
2. confines the path to FASTGPT_WORKDIR
38+
3. streams the file response
39+
```
40+
41+
## 4. Token Contract
42+
43+
Preview tokens reuse `AGENT_SANDBOX_PROXY_SECRET` and the existing business identity claims:
44+
45+
```ts
46+
{
47+
sourceType,
48+
sourceId,
49+
userId,
50+
chatId,
51+
channel: 'preview',
52+
permission: 'read',
53+
exp
54+
}
55+
```
56+
57+
The token never contains provider endpoints or the IDE agent password. The default lifetime remains two
58+
hours to preserve the existing `sandbox_get_file_url` contract.
59+
60+
## 5. HTTP Contracts
61+
62+
### 5.1 Public proxy
63+
64+
```text
65+
GET|HEAD /preview/{token}/{*path}
66+
```
67+
68+
- Preview may use an independent `PREVIEW_PORT`; when omitted it shares `PORT` with WebSocket routes.
69+
- `token` is one base64url JWT path segment.
70+
- `path` is workspace-relative.
71+
- Other methods return `405`.
72+
- The upstream host and port always come from FastGPT ticket resolution.
73+
74+
### 5.2 Sandbox preview listener
75+
76+
```text
77+
GET|HEAD /preview/{*path}
78+
X-FastGPT-Agent-Token: {agentPassword}
79+
```
80+
81+
- Binds to the fixed internal address `0.0.0.0:1319`.
82+
- Uses `FASTGPT_WORKDIR` as the root.
83+
- Does not expose directory listings.
84+
- Rejects absolute paths, traversal, invalid encodings, and symlinks escaping the workspace.
85+
86+
## 6. FastGPT Changes
87+
88+
- Add reusable preview token signing and URL construction in the sandbox application layer.
89+
- Require `AGENT_SANDBOX_PREVIEW_PROXY_URL` as the public HTTP preview origin. It remains separate
90+
from the required WebSocket `AGENT_SANDBOX_PROXY_URL` even when both URLs use the same port.
91+
- Change `getHtmlPreviewLink` to validate the session and return a preview URL without reading/uploading
92+
the HTML file.
93+
- Change `sandbox_get_file_url` to return preview URLs without reading/uploading the files.
94+
- Extend internal ticket verification with `channel=preview` and endpoint port `1319`.
95+
- Update OpenAPI descriptions and tool descriptions to remove S3 semantics.
96+
97+
## 7. Resource Reference Behavior
98+
99+
Given:
100+
101+
```text
102+
/preview/{token}/test/preview.html
103+
```
104+
105+
- `./assets/app.js` resolves to `/preview/{token}/test/assets/app.js` and works.
106+
- `../images/a.png` resolves to `/preview/{token}/images/a.png` and works.
107+
- `/assets/app.js` loses the token prefix and is intentionally unsupported in this path-based version.
108+
109+
The sandbox system prompt must require relative asset paths for generated previews.
110+
111+
## 8. Security
112+
113+
- A preview URL is a bearer capability for read-only access to the token's whole workspace.
114+
- Preview content uses the existing proxy origin, which must remain separate from the FastGPT app origin.
115+
- Responses set `Referrer-Policy: no-referrer`, `X-Content-Type-Options: nosniff`, and
116+
`Cache-Control: private, no-store`.
117+
- Proxy and ingress logs must redact the token path segment.
118+
- The proxy sends tickets to FastGPT in `X-Sandbox-Ticket`, not the query string, so the main app's
119+
request logs do not contain bearer tokens. FastGPT temporarily accepts the old query transport for rollout.
120+
- The proxy does not accept an upstream URL, host, scheme, or port from the client.
121+
- Cached endpoint resolution uses SHA-256 token keys and never bypasses local token expiration checks.
122+
123+
## 9. Compatibility And Rollout
124+
125+
- Sandbox runtime images must include the preview listener before FastGPT starts issuing preview URLs.
126+
- Default deployments expose one proxy port; split WebSocket and Preview ports only when explicitly configured.
127+
- Old sandboxes without port 1319 return a clear proxy error; local integration uses a freshly built image.
128+
- Upgrade FastGPT before agent-sandbox-proxy: the app accepts both old query tickets and new header tickets,
129+
while the new proxy only uses the non-logging header transport.
130+
- `fs`, `terminal`, editor upload/download, and workspace S3 archive paths remain unchanged.
131+
- Local verification builds worktree images and runs them against the existing Docker dependencies.
132+
133+
## 10. TODO
134+
135+
- [x] Implement and test the IDE agent preview HTTP listener.
136+
- [x] Implement and test proxy preview routing and streaming relay.
137+
- [x] Add preview token signing, ticket verification, and URL helpers.
138+
- [x] Replace HTML preview S3 upload.
139+
- [x] Replace `sandbox_get_file_url` S3 upload.
140+
- [x] Update OpenAPI, environment/runtime contracts, prompt, and rollout notes.
141+
- [x] Build local sandbox and proxy images.
142+
- [x] Run TypeScript and Rust unit tests.
143+
- [x] Run an OpenSandbox end-to-end test with HTML, CSS, image, HEAD, Range, expiry, auth, and missing files.
144+
- [x] Cover traversal and escaping symlinks in unit tests.
145+
- [x] Run repository-wide final validation.

.forgejo/workflows/test-rust-agent.yaml

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,15 @@ jobs:
2828
- uses: actions/checkout@v6
2929

3030
- name: Install Rust toolchain
31-
uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable
32-
with:
33-
components: clippy, rustfmt
34-
35-
- name: Rust Cache
36-
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
37-
with:
38-
shared-key: "fastgpt-rust-agent-cache"
39-
workspaces: |
40-
projects/agent-sandbox-proxy
41-
projects/fastgpt-ide-agent
31+
run: |
32+
if ! command -v rustup >/dev/null 2>&1; then
33+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
34+
| sh -s -- -y --profile minimal
35+
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
36+
export PATH="$HOME/.cargo/bin:$PATH"
37+
fi
38+
rustup toolchain install stable --profile minimal --component clippy,rustfmt
39+
rustup default stable
4240
4341
# --- agent-sandbox-proxy Checks & Tests ---
4442
- name: Run agent-sandbox-proxy fmt check

.github/workflows/test-rust-agent.yaml

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,15 @@ jobs:
2828
- uses: actions/checkout@v6
2929

3030
- name: Install Rust toolchain
31-
uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable
32-
with:
33-
components: clippy, rustfmt
34-
35-
- name: Rust Cache
36-
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
37-
with:
38-
shared-key: "fastgpt-rust-agent-cache"
39-
workspaces: |
40-
projects/agent-sandbox-proxy
41-
projects/fastgpt-ide-agent
31+
run: |
32+
if ! command -v rustup >/dev/null 2>&1; then
33+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
34+
| sh -s -- -y --profile minimal
35+
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
36+
export PATH="$HOME/.cargo/bin:$PATH"
37+
fi
38+
rustup toolchain install stable --profile minimal --component clippy,rustfmt
39+
rustup default stable
4240
4341
# --- agent-sandbox-proxy Checks & Tests ---
4442
- name: Run agent-sandbox-proxy fmt check

deploy/version/v4.15/docker-compose.template.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ x-agent-sandbox-config: &x-agent-sandbox-config
7575
AGENT_SANDBOX_PROXY_SECRET: *x-agent-sandbox-proxy-secret
7676
# 浏览器访问 agent-sandbox-proxy 的地址。启用 Agent/Skill 沙盒时,必须改成单独部署的 proxy 的 ws:// 或 wss:// 地址。
7777
AGENT_SANDBOX_PROXY_URL:
78+
# 浏览器访问 sandbox preview 的 http:// 或 https:// 地址。默认单端口部署时与 WebSocket 使用相同端口。
79+
AGENT_SANDBOX_PREVIEW_PROXY_URL:
7880
AGENT_SANDBOX_OPENSANDBOX_BASEURL:
7981
AGENT_SANDBOX_OPENSANDBOX_API_KEY:
8082
AGENT_SANDBOX_OPENSANDBOX_RUNTIME: docker

packages/global/core/ai/sandbox/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,5 @@ export const SANDBOX_SYSTEM_PROMPT = `## 沙盒能力
6262
- 使用 ${SANDBOX_FIND_TOOL_NAME} 按 glob 搜索文件路径,优先于通过 shell 调用 find
6363
- 使用 ${SANDBOX_LS_TOOL_NAME} 列出目录内容,优先于通过 shell 调用 ls
6464
- 默认将生成文件保存在当前 sandbox 工作目录;若本轮 system-reminder 指定了更具体的产物目录或禁止目录,必须优先遵守
65+
- HTML 等多文件预览产物必须使用相对资源路径(例如 ./assets/app.js),不要使用 /assets/app.js 这类根路径
6566
- 若需要将生成的文件链接,可使用 ${SANDBOX_GET_FILE_URL_TOOL_NAME} 获取临时访问链接`;

packages/global/core/ai/sandbox/tools/getFileUrl.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,17 @@ export const SANDBOX_GET_FILE_URL_TOOL: ChatCompletionTool = {
1313
type: 'function',
1414
function: {
1515
name: SANDBOX_GET_FILE_URL_TOOL_NAME,
16-
description: '从虚拟机读取指定文件,上传至云存储,返回 2 小时有效期的访问链接',
16+
description: '为虚拟机 workspace 中的指定文件返回 2 小时有效期的直连访问链接',
1717
parameters: {
1818
type: 'object',
1919
properties: {
2020
paths: {
2121
type: 'array',
2222
items: {
2323
type: 'string',
24-
description: '文件访问路径,例如: output.csv'
24+
description: 'workspace 文件路径,例如: output.csv'
2525
},
26-
description: '文件访问路径,例如: ["output.csv", "output.txt"]'
26+
description: 'workspace 文件路径,例如: ["output.csv", "output.txt"]'
2727
}
2828
},
2929
required: ['paths']

packages/global/openapi/core/ai/sandbox/api.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,12 +131,21 @@ export type SandboxGetTicketResponse = z.infer<typeof SandboxGetTicketResponseSc
131131
*/
132132
export const SandboxGetHtmlPreviewLinkBodyRawSchema = createOutLinkChatTargetInputSchema({
133133
...SandboxBaseShape,
134-
filePath: z.string().describe('当前 Chat Session 下的 HTML 文件路径')
134+
filePath: z.string().meta({
135+
example: 'dist/index.html',
136+
description: '当前 Chat Session 下的 HTML 文件路径'
137+
})
135138
});
136139
export const SandboxGetHtmlPreviewLinkBodySchema = withSandboxTarget({
137-
filePath: z.string().describe('当前 Chat Session 下的 HTML 文件路径')
140+
filePath: z.string().meta({
141+
example: 'dist/index.html',
142+
description: '当前 Chat Session 下的 HTML 文件路径'
143+
})
144+
});
145+
export const SandboxGetHtmlPreviewLinkResponseSchema = z.string().url().meta({
146+
example: 'https://agent-proxy.example.com/preview/token/dist/index.html',
147+
description: '由 agent-proxy 直接读取 sandbox workspace 的短期 HTML 预览链接'
138148
});
139-
export const SandboxGetHtmlPreviewLinkResponseSchema = z.string().describe('HTML 预览链接');
140149
export type SandboxGetHtmlPreviewLinkBody = z.input<typeof SandboxGetHtmlPreviewLinkBodySchema>;
141150
export type SandboxGetHtmlPreviewLinkRuntimeBody = z.output<
142151
typeof SandboxGetHtmlPreviewLinkBodySchema

packages/global/openapi/core/ai/sandbox/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ export const SandboxPath: OpenAPIPath = {
7070
'/core/ai/sandbox/getHtmlPreviewLink': {
7171
post: {
7272
summary: '获取 HTML 文件预览链接',
73-
description: '返回用于在浏览器中预览 HTML 文件的链接(S3 托管)',
73+
description:
74+
'校验文件后签发短期只读链接,由 agent-proxy 直接转发 sandbox workspace 内容,不复制到对象存储',
7475
tags: [DevApiTagsMap.sandbox],
7576
requestBody: {
7677
content: {

0 commit comments

Comments
 (0)