Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/service/core/agentSkills/sandboxConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export function getSandboxDefaults(): SandboxDefaults {
workDirectory: '/home/sandbox/workspace',
// workDirectory: env.AGENT_SANDBOX_OPENSANDBOX_WORK_DIRECTORY ?? '/home/sandbox/workspace',
targetPort: 44772,
entrypoint: '/home/sandbox/entrypoint.sh'
entrypoint: '/opt/entrypoint.sh'
// entrypoint: env.AGENT_SANDBOX_OPENSANDBOX_ENTRYPOINT ?? '/home/sandbox/entrypoint.sh'
};
}
Expand Down
15 changes: 8 additions & 7 deletions projects/agent-sandbox/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,22 @@ RUN rm -rf /var/lib/apt/lists/*
# Install code-server using the official script
RUN curl -fsSL https://code-server.dev/install.sh | sh

# Copy and configure entrypoint
COPY entrypoint.sh /opt/entrypoint.sh
RUN chmod +x /opt/entrypoint.sh

# Create a non-root user for security
RUN useradd --create-home --shell /bin/bash sandbox

USER root
USER sandbox

WORKDIR /home/sandbox

# Copy VS Code settings
RUN mkdir -p /home/sandbox/.local/share/code-server/User
COPY --chown=sandbox:sandbox settings.json /home/sandbox/.local/share/code-server/User/settings.json

# Copy and configure entrypoint
COPY --chown=sandbox:sandbox entrypoint.sh /home/sandbox/entrypoint.sh
RUN chmod +x /home/sandbox/entrypoint.sh
COPY settings.json /home/sandbox/.local/share/code-server/User/settings.json

# Expose code-server port
EXPOSE 8080

ENTRYPOINT ["/home/sandbox/entrypoint.sh"]
ENTRYPOINT ["/opt/entrypoint.sh"]
2 changes: 1 addition & 1 deletion projects/agent-sandbox/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ if [ "${_ENABLE_CODE_SERVER}" = "true" ]; then
--disable-workspace-trust \
--disable-getting-started-override \
--app-name "Skills" \
--user-data-dir /home/sandbox/.local/share/code-server \
--user-data-dir ~/.local/share/code-server \
"${WORKDIR}"
else
exec sleep infinity
Expand Down
34 changes: 19 additions & 15 deletions projects/app/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,15 @@ async function main() {
)) as typeof import('./src/service/core/sandbox/proxyUtils');

// Fetch the code-server password from the container config.yaml via the internal API.
async function fetchCodeServerPassword(sandboxId: string): Promise<string | null> {
async function fetchCodeServerPassword(
sandboxId: string,
teamId: string
): Promise<string | null> {
try {
const resp = await fetch(`http://127.0.0.1:${port}/api/core/sandbox/proxyCSPassword`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ sandboxId })
body: JSON.stringify({ sandboxId, teamId })
});
if (!resp.ok) return null;
const { password } = await resp.json();
Expand Down Expand Up @@ -85,10 +88,11 @@ async function main() {
async function injectCodeServerAuth(
reqHeaders: IncomingMessage['headers'],
sandboxId: string,
target: string
target: string,
teamId: string
): Promise<void> {
const key = await ensureCodeServerSession(sandboxId, target, () =>
fetchCodeServerPassword(sandboxId)
fetchCodeServerPassword(sandboxId, teamId)
);
if (key) injectCsKey(reqHeaders, key);
}
Expand Down Expand Up @@ -173,15 +177,15 @@ async function main() {
proxyType: string
) {
try {
const target = await authProxyTarget(req.headers, sandboxId, portNum);
const { target, teamId } = await authProxyTarget(req.headers, sandboxId, portNum);
const csTarget = deriveCsLoginTarget(target, req.url || '');
if (proxyType === 'absproxy') {
await injectCodeServerAuth(req.headers, sandboxId, csTarget);
await injectCodeServerAuth(req.headers, sandboxId, csTarget, teamId);
await handleAbsProxy(req, res, target, sandboxId, String(portNum));
} else {
// Rewrite Origin so code-server's CSRF check passes (changeOrigin only rewrites Host).
const targetUrl = new URL(target);
await injectCodeServerAuth(req.headers, sandboxId, csTarget);
await injectCodeServerAuth(req.headers, sandboxId, csTarget, teamId);
// Mark the request so the proxyRes handler can identify the sandbox on session expiry.
req.headers['x-fastgpt-sandbox-id'] = sandboxId;
proxy.web(req, res, {
Expand Down Expand Up @@ -231,10 +235,10 @@ async function main() {
}

try {
const target = await authProxyTarget(req.headers, sandboxId, portNum);
const { target, teamId } = await authProxyTarget(req.headers, sandboxId, portNum);
const targetUrl = new URL(target);
const csTarget = deriveCsLoginTarget(target, req.url || '');
await injectCodeServerAuth(req.headers, sandboxId, csTarget);
await injectCodeServerAuth(req.headers, sandboxId, csTarget, teamId);
req.headers['x-fastgpt-sandbox-id'] = sandboxId;
proxy.web(req, res, {
target,
Expand Down Expand Up @@ -304,7 +308,7 @@ async function main() {

let target: string;
try {
target = await authProxyTarget(req.headers, tunnelSandboxId, tunnelPort);
({ target } = await authProxyTarget(req.headers, tunnelSandboxId, tunnelPort));
dev && console.log(`[proxy:tcptunnel] auth ok target=${target}`);
} catch (err: any) {
const status = err.statusCode || 502;
Expand Down Expand Up @@ -426,13 +430,13 @@ async function main() {
);

try {
const target = await authProxyTarget(req.headers, sandboxId, portNum);
const { target, teamId } = await authProxyTarget(req.headers, sandboxId, portNum);
dev && console.log(`[proxy:ws] auth ok, forwarding to target=${target}`);
// Rewrite Origin to match the target host so code-server's CSRF check passes.
// changeOrigin:true only rewrites Host, not Origin.
const targetUrl = new URL(target);
const csTarget = deriveCsLoginTarget(target, req.url || '');
await injectCodeServerAuth(req.headers, sandboxId, csTarget);
await injectCodeServerAuth(req.headers, sandboxId, csTarget, teamId);
req.headers['x-fastgpt-sandbox-id'] = sandboxId;
proxy.ws(req, socket, head, {
target,
Expand All @@ -457,7 +461,7 @@ async function authProxyTarget(
reqHeaders: IncomingMessage['headers'],
sandboxId: string,
targetPort: number
): Promise<string> {
): Promise<{ target: string; teamId: string }> {
dev &&
console.log(
`[proxy:auth] POST proxyAuth sandboxId=${sandboxId} port=${targetPort} hasCookie=${!!reqHeaders.cookie}`
Expand All @@ -483,9 +487,9 @@ async function authProxyTarget(
throw Object.assign(new Error(msg), { statusCode: code });
}

const { target } = await authResp.json();
const { target, teamId } = await authResp.json();
dev && console.log(`[proxy:auth] proxyAuth ok target=${target}`);
return target as string;
return { target: target as string, teamId: teamId as string };
}

// Build upstream request headers, dropping hop-by-hop headers
Expand Down
8 changes: 6 additions & 2 deletions projects/app/src/pages/api/core/sandbox/proxyAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
return Promise.reject('Missing sandboxId or targetPort');
}

const target = await getSandboxProxyTarget(req.headers, sandboxId, Number(targetPort));
return res.json({ target });
const { target, teamId } = await getSandboxProxyTarget(
req.headers,
sandboxId,
Number(targetPort)
);
return res.json({ target, teamId });
}

// GET handler: redirect flow for subdomain cookie hand-off.
Expand Down
6 changes: 3 additions & 3 deletions projects/app/src/pages/api/core/sandbox/proxyCSPassword.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
return res.status(403).json({ error: 'Internal only' });
}

const { sandboxId } = req.body as { sandboxId?: string };
if (!sandboxId) return res.status(400).json({ error: 'Missing sandboxId' });
const { sandboxId, teamId } = req.body as { sandboxId?: string; teamId?: string };
if (!sandboxId || !teamId) return res.status(400).json({ error: 'Missing sandboxId or teamId' });

const password = await getCodeServerPasswordFromSandbox(sandboxId);
const password = await getCodeServerPasswordFromSandbox(sandboxId, teamId);
return res.json({ password });
}

Expand Down
18 changes: 14 additions & 4 deletions projects/app/src/service/core/sandbox/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export async function getSandboxProxyTarget(
headers: IncomingHttpHeaders,
sandboxId: string,
targetPort: number
): Promise<string> {
): Promise<{ target: string; teamId: string }> {
if (targetPort < 1 || targetPort > 65535) {
throw Object.assign(new Error('Invalid port'), { statusCode: 400 });
}
Expand All @@ -39,7 +39,10 @@ export async function getSandboxProxyTarget(
const session = getProxySession(sandboxId);
if (session) {
dev && console.log(`[sandboxProxy] session fallback sandboxId=${sandboxId}`);
return `${session.protocol}://${session.host}:${targetPort}`;
return {
target: `${session.protocol}://${session.host}:${targetPort}`,
teamId: session.teamId
};
}
throw Object.assign(new Error('Unauthorized'), { statusCode: 401 });
}
Expand All @@ -62,17 +65,24 @@ export async function getSandboxProxyTarget(
const { host, protocol } = sandbox.metadata!.endpoint!;
// Cache target for subsequent cookie-less sub-requests (sandboxed iframe)
upsertProxySession(sandboxId, authTeamId, host, protocol);
return `${protocol}://${host}:${targetPort}`;
return { target: `${protocol}://${host}:${targetPort}`, teamId: authTeamId };
}

/**
* Read the code-server password from the container's config.yaml via exec.
* Returns null if the sandbox is not found, has no providerSandboxId, or exec fails.
*/
export async function getCodeServerPasswordFromSandbox(sandboxId: string): Promise<string | null> {
export async function getCodeServerPasswordFromSandbox(
sandboxId: string,
teamId: string
): Promise<string | null> {
const sandbox = await MongoSandboxInstance.findOne({ sandboxId }).lean();
if (!sandbox?.metadata?.providerSandboxId) return null;

if (String(sandbox.metadata?.teamId) !== teamId) {
throw Object.assign(new Error('Access denied'), { statusCode: 403 });
}

const providerConfig = getSandboxProviderConfig();
const adapter = await connectToProviderSandbox(
providerConfig,
Expand Down
Loading