Repository navigation
feat(desktop,cli): switch to let your cloud workspaces reach a host #9409
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
0bbe43c
80deee6
5fc716a
b6d5aae
8471ca3
fc191ba
61091fa
8d377c3
4119904
5cd743c
a65d1b4
024534b
f41d68c
9f81d6f
ac43e15
1645e2c
88603be
8e240f4
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 |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import { Trans, useLingui } from "@lingui/react/macro"; | ||
| import { Label } from "@superset/ui/label"; | ||
| import { HighlightText } from "renderer/routes/_authenticated/settings/components/HighlightText"; | ||
| import { useSettingsSearchQuery } from "renderer/stores/settings-state"; | ||
| import { CloudWorkspaceAccessSwitch } from "./components/CloudWorkspaceAccessSwitch"; | ||
|
|
||
| interface CloudWorkspaceAccessSectionProps { | ||
| organizationId: string; | ||
| machineId: string; | ||
| allowed: boolean; | ||
| } | ||
|
|
||
| export function CloudWorkspaceAccessSection({ | ||
| organizationId, | ||
| machineId, | ||
| allowed, | ||
| }: CloudWorkspaceAccessSectionProps) { | ||
| const { t } = useLingui(); | ||
| const searchQuery = useSettingsSearchQuery(); | ||
|
|
||
| return ( | ||
| <div className="flex items-start justify-between gap-6"> | ||
| <div className="space-y-1 flex-1"> | ||
| <Label htmlFor="allow-cloud-workspaces" className="text-sm font-medium"> | ||
| <HighlightText | ||
| text={t({ | ||
| message: "Allow my cloud workspaces to reach this host", | ||
| })} | ||
| query={searchQuery} | ||
| /> | ||
| </Label> | ||
| <p className="text-xs text-muted-foreground"> | ||
| <Trans> | ||
| Agents in your cloud workspaces can then use <code>--host</code> to | ||
| run commands on this host. The host must be reachable through the | ||
| relay. | ||
| </Trans> | ||
| </p> | ||
| </div> | ||
| <CloudWorkspaceAccessSwitch | ||
| id="allow-cloud-workspaces" | ||
| organizationId={organizationId} | ||
| machineId={machineId} | ||
| allowed={allowed} | ||
| /> | ||
| </div> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| import { useLingui } from "@lingui/react/macro"; | ||
| import { errorMessage } from "@superset/i18n/errors"; | ||
| import { toast } from "@superset/ui/sonner"; | ||
| import { Switch } from "@superset/ui/switch"; | ||
| import { useState } from "react"; | ||
| import { cloudTrpc } from "renderer/lib/cloud-trpc"; | ||
| import { CloudWorkspaceAccessConfirmDialog } from "./components/CloudWorkspaceAccessConfirmDialog"; | ||
|
|
||
| interface CloudWorkspaceAccessSwitchProps { | ||
| id: string; | ||
| organizationId: string; | ||
| machineId: string; | ||
| allowed: boolean; | ||
| } | ||
|
|
||
| export function CloudWorkspaceAccessSwitch({ | ||
| id, | ||
| organizationId, | ||
| machineId, | ||
| allowed, | ||
| }: CloudWorkspaceAccessSwitchProps) { | ||
| const { t } = useLingui(); | ||
| const utils = cloudTrpc.useUtils(); | ||
| const setAccess = cloudTrpc.host.setCloudWorkspaceAccess.useMutation({ | ||
| onSettled: () => utils.host.roster.invalidate(), | ||
| }); | ||
| const [confirmOpen, setConfirmOpen] = useState(false); | ||
| const [confirmTarget, setConfirmTarget] = useState(false); | ||
|
|
||
| const runToggle = (next: boolean) => { | ||
| toast.promise( | ||
| setAccess.mutateAsync({ organizationId, machineId, allowed: next }), | ||
| { | ||
| loading: t({ message: "Saving…" }), | ||
| success: () => t({ message: "Setting saved" }), | ||
| error: (err: unknown) => | ||
| errorMessage(err, t({ message: "Failed to update setting" })), | ||
| }, | ||
| ); | ||
| }; | ||
|
|
||
| return ( | ||
| <> | ||
| <Switch | ||
| id={id} | ||
| checked={allowed} | ||
| onCheckedChange={(next) => { | ||
| setConfirmTarget(next); | ||
| setConfirmOpen(true); | ||
| }} | ||
| disabled={setAccess.isPending} | ||
| /> | ||
| <CloudWorkspaceAccessConfirmDialog | ||
| open={confirmOpen} | ||
| targetEnabled={confirmTarget} | ||
| onOpenChange={setConfirmOpen} | ||
| onConfirm={() => { | ||
| setConfirmOpen(false); | ||
| runToggle(confirmTarget); | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| }} | ||
| /> | ||
| </> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| import { Trans } from "@lingui/react/macro"; | ||
| import { | ||
| AlertDialog, | ||
| AlertDialogContent, | ||
| AlertDialogDescription, | ||
| AlertDialogFooter, | ||
| AlertDialogHeader, | ||
| AlertDialogTitle, | ||
| } from "@superset/ui/alert-dialog"; | ||
| import { Button } from "@superset/ui/button"; | ||
|
|
||
| interface CloudWorkspaceAccessConfirmDialogProps { | ||
| open: boolean; | ||
| targetEnabled: boolean; | ||
| onOpenChange: (open: boolean) => void; | ||
| onConfirm: () => void; | ||
| } | ||
|
|
||
| export function CloudWorkspaceAccessConfirmDialog({ | ||
| open, | ||
| targetEnabled, | ||
| onOpenChange, | ||
| onConfirm, | ||
| }: CloudWorkspaceAccessConfirmDialogProps) { | ||
| return ( | ||
| <AlertDialog open={open} onOpenChange={onOpenChange}> | ||
| <AlertDialogContent className="max-w-[480px]"> | ||
| <AlertDialogHeader> | ||
| <AlertDialogTitle> | ||
| {targetEnabled ? ( | ||
| <Trans>Let your cloud workspaces reach this host?</Trans> | ||
| ) : ( | ||
| <Trans>Stop your cloud workspaces from reaching this host?</Trans> | ||
| )} | ||
| </AlertDialogTitle> | ||
| <AlertDialogDescription asChild> | ||
| <div className="space-y-3 text-sm text-muted-foreground"> | ||
| {targetEnabled ? ( | ||
| <p> | ||
| <Trans> | ||
| Anything running in a cloud workspace you created can then | ||
| create workspaces, start agents, and run commands on this | ||
| host, including an agent following instructions it found in | ||
| a web page or a repository. Turn this on only if you need | ||
| it. | ||
| </Trans> | ||
| </p> | ||
| ) : ( | ||
| <p> | ||
| <Trans> | ||
| Your cloud workspaces will no longer be able to reach this | ||
| host. | ||
| </Trans> | ||
| </p> | ||
| )} | ||
| </div> | ||
| </AlertDialogDescription> | ||
| </AlertDialogHeader> | ||
| <AlertDialogFooter> | ||
| <Button variant="ghost" size="sm" onClick={() => onOpenChange(false)}> | ||
| <Trans>Cancel</Trans> | ||
| </Button> | ||
| <Button | ||
| variant={targetEnabled ? "destructive" : "default"} | ||
| size="sm" | ||
| onClick={onConfirm} | ||
| > | ||
| {targetEnabled ? <Trans>Allow</Trans> : <Trans>Turn off</Trans>} | ||
| </Button> | ||
| </AlertDialogFooter> | ||
| </AlertDialogContent> | ||
| </AlertDialog> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { CloudWorkspaceAccessConfirmDialog } from "./CloudWorkspaceAccessConfirmDialog"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { CloudWorkspaceAccessSwitch } from "./CloudWorkspaceAccessSwitch"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { CloudWorkspaceAccessSection } from "./CloudWorkspaceAccessSection"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -111,6 +111,7 @@ export const SETTING_ITEM_ID = { | |
| "security-expose-host-service-via-relay", | ||
|
|
||
| HOST_MEMBERS: "host-members", | ||
| HOST_ALLOW_CLOUD_WORKSPACES: "host-allow-cloud-workspaces", | ||
| ENVIRONMENTS_LIST: "environments-list", | ||
| AGENT_ACCOUNTS: "agent-accounts", | ||
| CONNECTIONS: "connections", | ||
|
|
@@ -274,6 +275,7 @@ export const SETTING_ITEM_VARIANT: Record<SettingItemId, SettingVariant> = { | |
| [SETTING_ITEM_ID.MOBILE_APP]: "shared", | ||
|
|
||
| [SETTING_ITEM_ID.HOST_MEMBERS]: "shared", | ||
| [SETTING_ITEM_ID.HOST_ALLOW_CLOUD_WORKSPACES]: "shared", | ||
| [SETTING_ITEM_ID.ENVIRONMENTS_LIST]: "v2", | ||
| [SETTING_ITEM_ID.AGENT_ACCOUNTS]: "v2", | ||
| [SETTING_ITEM_ID.CONNECTIONS]: "v2", | ||
|
|
@@ -1912,6 +1914,27 @@ export const SETTINGS_ITEMS: SettingsItem[] = [ | |
| "attack surface", | ||
| ], | ||
| }, | ||
| { | ||
| id: SETTING_ITEM_ID.HOST_ALLOW_CLOUD_WORKSPACES, | ||
| section: "hosts", | ||
|
Comment on lines
+1918
to
+1919
Contributor
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.
|
||
| title: "Allow my cloud workspaces to reach this host", | ||
| description: | ||
| "Lets agents in your cloud workspaces call a host you own through the relay", | ||
| keywords: [ | ||
| "security", | ||
| "cloud", | ||
| "cloud workspace", | ||
| "sandbox", | ||
| "relay", | ||
| "remote", | ||
| "--host", | ||
| "agent", | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
|
||
| "host", | ||
| "hosts", | ||
| "access", | ||
| "owner", | ||
| ], | ||
| }, | ||
| { | ||
| id: SETTING_ITEM_ID.AGENT_ACCOUNTS, | ||
| section: "agentAccounts", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import { afterAll, describe, expect, test } from "bun:test"; | ||
| import type { ApiClient } from "../api-client"; | ||
| import { resolveHostTarget } from "./resolveHostTarget"; | ||
|
|
||
| const seen: Headers[] = []; | ||
| const relay = Bun.serve({ | ||
| port: 0, | ||
| fetch: (request) => { | ||
| seen.push(request.headers); | ||
| return Response.json([{ result: { data: { json: [] } } }]); | ||
| }, | ||
| }); | ||
| afterAll(() => relay.stop(true)); | ||
|
|
||
| const api = { | ||
| host: { | ||
| relayEndpoint: { query: async () => ({ url: relay.url.origin }) }, | ||
| }, | ||
| } as unknown as ApiClient; | ||
|
|
||
| describe("resolveHostTarget", () => { | ||
| test("a cloud workspace calls the relay with no bearer, so its firewall-added credential is what counts", async () => { | ||
| const target = await resolveHostTarget({ | ||
| requestedHostId: "some-other-host", | ||
| organizationId: "1c0587c9-a74c-423c-ae40-b97296ad9b27", | ||
| userJwt: "", | ||
| api, | ||
| }); | ||
|
|
||
| await target.client.workspace.list.query(); | ||
|
|
||
| expect(target.kind).toBe("remote"); | ||
| expect(seen.at(-1)?.get("authorization")).toBeNull(); | ||
| }); | ||
|
|
||
| test("a signed-in caller still sends its bearer to the relay", async () => { | ||
| const target = await resolveHostTarget({ | ||
| requestedHostId: "some-other-host", | ||
| organizationId: "1c0587c9-a74c-423c-ae40-b97296ad9b27", | ||
| userJwt: "user-jwt", | ||
| api, | ||
| }); | ||
|
|
||
| await target.client.workspace.list.query(); | ||
|
|
||
| expect(seen.at(-1)?.get("authorization")).toBe("Bearer user-jwt"); | ||
| }); | ||
| }); |
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.
P3: Apply the settings-item visibility check to this section so unrelated searches, such as “Members,” hide the cloud-workspace control.
Prompt for AI agents
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.
Not changing this; same as the CodeRabbit thread above. No section on the host settings page filters by the search query (Members, Worktrees and Remote access all stay visible), so filtering only this one would be inconsistent.