Skip to content
Draft
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

324 changes: 211 additions & 113 deletions apps/desktop/src-tauri/src/main.rs

Large diffs are not rendered by default.

29 changes: 20 additions & 9 deletions apps/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5963,6 +5963,7 @@ function SettingsView({
const [localSettings, setLocalSettings] = useState(snapshot.settings);
const [portableApiUrl, setPortableApiUrl] = useState("");
const [portableRoot, setPortableRoot] = useState("");
const [portableCredentialRef, setPortableCredentialRef] = useState("hosted-workspace:desktop");
const [portableProfileKey, setPortableProfileKey] = useState("");
const [portableWorkspaceState, setPortableWorkspaceState] = useState<
"idle" | "materializing" | "success" | "error"
Expand Down Expand Up @@ -6152,6 +6153,7 @@ function SettingsView({
const validation = validatePortableWorkspaceForm({
apiUrl: portableApiUrl,
root: portableRoot,
credentialRef: portableCredentialRef,
profileKey: portableProfileKey,
});
if (!validation.ok) {
Expand All @@ -6160,18 +6162,18 @@ function SettingsView({
return;
}
setPortableWorkspaceState("materializing");
setPortableWorkspaceMessage("Negotiating and materializing the hosted workspace…");
setPortableWorkspaceMessage("Negotiating and attaching the hosted workspace…");
try {
const fallback: PortableWorkspaceReport = {
ok: true,
api_origin: validation.request.apiUrl,
profile_id: "018f4f6e-9f2c-7b1a-8c3d-4e5f60718293",
profile_revision: 1,
root: validation.request.root,
session_id: "demo-session",
content_encoding: "identity",
entries: 0,
mount_count: 0,
files: 0,
directories: 0,
materialized_bytes: 0,
decoded_bytes: 0,
};
const report = await invokePortableWorkspace(
(command, args) => callCommand<PortableWorkspaceReport>(command, args, fallback),
Expand All @@ -6189,7 +6191,7 @@ function SettingsView({
const settingsSections: Array<{ id: SettingsSection; label: string; description: string }> = [
{ id: "general", label: "General", description: "Startup and desktop behavior" },
{ id: "sources", label: "Sources", description: "Connected workspaces and local folders" },
{ id: "hosted", label: "Hosted (preview)", description: "Manual portable materializer" },
{ id: "hosted", label: "Hosted", description: "Durable hosted workspace attachment" },
{ id: "sync", label: "Sync", description: "Live Mode and review policy" },
{ id: "activity", label: "Activity", description: "Recent events and debug queue" },
{ id: "agents", label: "Agents", description: "Local agent instructions" },
Expand Down Expand Up @@ -6275,9 +6277,9 @@ function SettingsView({

{settingsSection === "hosted" && (
<section className="panel settings-section-panel portable-workspace-panel">
<PanelTitle title="Hosted materializer preview" />
<PanelTitle title="Hosted workspace" />
<p className="quiet-note">
Manually materialize or recover a generation-2 workspace. This preview is not yet bound to Desktop sources, mounts, or stored credentials.
Attach a generation-2 workspace durably. Desktop and the loc CLI share the same attachment, refresh, recovery, and relocation coordinator.
</p>
<div className="portable-workspace-fields">
<label className="source-inline-field">
Expand All @@ -6299,6 +6301,15 @@ function SettingsView({
onChange={(event) => setPortableRoot(event.target.value)}
/>
</label>
<label className="source-inline-field">
<span>Credential reference</span>
<input
value={portableCredentialRef}
placeholder="hosted-workspace:desktop"
disabled={portableWorkspaceState === "materializing"}
onChange={(event) => setPortableCredentialRef(event.target.value)}
/>
</label>
<label className="source-inline-field portable-workspace-key-field">
<span>Workspace Profile key</span>
<input
Expand All @@ -6322,7 +6333,7 @@ function SettingsView({
disabled={portableWorkspaceState === "materializing"}
onClick={() => void materializePortableWorkspace()}
>
{portableWorkspaceState === "materializing" ? "Materializing" : "Materialize Workspace"}
{portableWorkspaceState === "materializing" ? "Attaching" : "Attach Workspace"}
</PrimaryButton>
{portableWorkspaceMessage && (
<p
Expand Down
105 changes: 63 additions & 42 deletions apps/desktop/src/portable-workspace.test.ts
Original file line number Diff line number Diff line change
@@ -1,107 +1,128 @@
import { describe, expect, it } from "vitest";
import {
hostedWorkspaceCommand,
invokeHostedWorkspace,
invokeHostedWorkspaceList,
invokePortableWorkspace,
portableWorkspaceSuccessMessage,
validatePortableWorkspaceForm,
workspaceWorkflowCommand,
} from "./portable-workspace";

describe("hosted portable workspace", () => {
it("builds the exact Desktop command request", async () => {
expect(validatePortableWorkspaceForm({
it("builds the exact attach request without a parallel materializer command", async () => {
const validation = validatePortableWorkspaceForm({
apiUrl: "https://workspace.example.test",
root: "/mnt/locality",
credentialRef: "hosted-workspace:desktop-team",
profileKey: "a".repeat(64),
})).toEqual({
});
expect(validation).toEqual({
ok: true,
request: {
apiUrl: "https://workspace.example.test",
root: "/mnt/locality",
credentialRef: "hosted-workspace:desktop-team",
profileKey: "a".repeat(64),
},
});
if (!validation.ok) throw new Error("expected valid request");

const calls: unknown[] = [];
const request = {
apiUrl: "https://workspace.example.test",
root: "/mnt/locality",
profileKey: "a".repeat(64),
};
await invokePortableWorkspace(async (command, args) => {
calls.push({ command, args });
return report();
}, request);
}, validation.request);
expect(calls).toEqual([{
command: "materialize_portable_workspace",
args: { request },
command: "attach_hosted_workspace",
args: { request: validation.request },
}]);
expect(workspaceWorkflowCommand("hosted")).toBe("attach_hosted_workspace");
expect(workspaceWorkflowCommand("local")).toBe("create_workspace_mount");
});

it("keeps hosted materialization separate from the existing local mount command", () => {
expect(workspaceWorkflowCommand("hosted")).toBe("materialize_portable_workspace");
expect(workspaceWorkflowCommand("local")).toBe("create_workspace_mount");
it("maps attach, refresh, relocate, and list to the shared coordinator IPC contract", async () => {
expect(hostedWorkspaceCommand("attach")).toBe("attach_hosted_workspace");
expect(hostedWorkspaceCommand("refresh")).toBe("refresh_hosted_workspace");
expect(hostedWorkspaceCommand("relocate")).toBe("relocate_hosted_workspace");

const request = {
apiUrl: "https://workspace.example.test",
root: "/mnt/relocated",
credentialRef: "hosted-workspace:desktop-team",
};
const calls: unknown[] = [];
await invokeHostedWorkspace(async (command, args) => {
calls.push({ command, args });
return report();
}, "refresh", request);
await invokeHostedWorkspace(async (command, args) => {
calls.push({ command, args });
return report();
}, "relocate", request);
await invokeHostedWorkspaceList(async (command) => {
calls.push({ command });
return { ok: true, attachments: [] };
});
expect(calls).toEqual([
{ command: "refresh_hosted_workspace", args: { request } },
{ command: "relocate_hosted_workspace", args: { request } },
{ command: "list_hosted_workspaces" },
]);
});

it("rejects incomplete or malformed hosted credentials before invoking Tauri", () => {
expect(validatePortableWorkspaceForm({
apiUrl: "file:///tmp/workspace",
root: "/mnt/locality",
profileKey: "a".repeat(64),
})).toEqual({ ok: false, message: "The hosted workspace API URL must use HTTP or HTTPS." });
expect(validatePortableWorkspaceForm({
it("rejects malformed placement, references, and credentials before invoking Tauri", () => {
const base = {
apiUrl: "https://workspace.example.test",
root: "/mnt/locality",
profileKey: "secret",
})).toEqual({
credentialRef: "hosted-workspace:desktop-team",
profileKey: "a".repeat(64),
};
expect(validatePortableWorkspaceForm({ ...base, credentialRef: "plain-ref" })).toEqual({
ok: false,
message: "Enter a valid hosted-workspace credential reference.",
});
expect(validatePortableWorkspaceForm({ ...base, profileKey: "secret" })).toEqual({
ok: false,
message: "The Workspace Profile key must be 64 lowercase hexadecimal characters.",
});
expect(validatePortableWorkspaceForm({ ...base, root: "relative/Locality" })).toEqual({
ok: false,
message: "The local workspace root must be an absolute path.",
});
});

it.each([
["https://workspace.example.test/api", "The hosted workspace API URL must not contain a path."],
["https://user@workspace.example.test", "The hosted workspace API URL must not contain credentials."],
["https://workspace.example.test?tenant=7", "The hosted workspace API URL must not contain a query or fragment."],
["https://workspace.example.test#tenant", "The hosted workspace API URL must not contain a query or fragment."],
["http://workspace.example.test", "HTTP is allowed only for a loopback hosted workspace."],
])("matches Rust URL rejection for %s", (apiUrl, message) => {
expect(validatePortableWorkspaceForm({
apiUrl,
root: "/mnt/locality",
credentialRef: "hosted-workspace:desktop-team",
profileKey: "a".repeat(64),
})).toEqual({ ok: false, message });
});

it("accepts loopback HTTP and rejects relative roots", () => {
expect(validatePortableWorkspaceForm({
apiUrl: "http://127.1:8080",
root: "/mnt/locality",
profileKey: "a".repeat(64),
}).ok).toBe(true);
expect(validatePortableWorkspaceForm({
apiUrl: "https://workspace.example.test",
root: "relative/Locality",
profileKey: "a".repeat(64),
})).toEqual({ ok: false, message: "The local workspace root must be an absolute path." });
});

it("renders completion state without exposing the profile key", () => {
const message = portableWorkspaceSuccessMessage(report());
expect(message).toBe("Materialized 4 file(s) and 3 folder(s) at /mnt/locality.");
expect(message).toBe("Attached 4 file(s) and 3 folder(s) at /mnt/locality.");
expect(message).not.toContain("secret");
});
});

function report() {
return {
ok: true,
api_origin: "https://workspace.example.test",
profile_id: "018f4f6e-9f2c-7b1a-8c3d-4e5f60718293",
profile_revision: 7,
root: "/mnt/locality",
session_id: "session-7",
content_encoding: "zstd",
entries: 8,
mount_count: 2,
files: 4,
directories: 3,
materialized_bytes: 120,
decoded_bytes: 4096,
};
}
65 changes: 56 additions & 9 deletions apps/desktop/src/portable-workspace.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,70 @@
export type PortableWorkspaceForm = {
apiUrl: string;
root: string;
credentialRef: string;
profileKey: string;
};

export type HostedWorkspaceOperation = "attach" | "refresh" | "relocate";

export type PortableWorkspaceRequest = {
apiUrl: string;
root: string;
profileKey: string;
credentialRef: string;
profileKey?: string;
};

export type PortableWorkspaceReport = {
ok: boolean;
api_origin: string;
profile_id: string;
profile_revision: number;
root: string;
session_id: string;
content_encoding: string;
entries: number;
mount_count: number;
files: number;
directories: number;
materialized_bytes: number;
decoded_bytes: number;
};

export type HostedWorkspaceListReport = {
ok: boolean;
attachments: Array<{
api_origin: string;
profile_id: string;
profile_revision: number;
root: string;
layout_version: number;
layout_digest: string;
mounts: Array<{
portable_mount_id: string;
local_mount_id: string;
mount_target: string;
active: boolean;
}>;
}>;
};

export type PortableWorkspaceValidation =
| { ok: true; request: PortableWorkspaceRequest }
| { ok: false; message: string };

export function workspaceWorkflowCommand(mode: "local" | "hosted"): string {
return mode === "hosted" ? "materialize_portable_workspace" : "create_workspace_mount";
return mode === "hosted" ? "attach_hosted_workspace" : "create_workspace_mount";
}

export function hostedWorkspaceCommand(operation: HostedWorkspaceOperation): string {
return `${operation}_hosted_workspace`;
}

export function invokeHostedWorkspace(
invoker: (
command: string,
args: { request: PortableWorkspaceRequest },
) => Promise<PortableWorkspaceReport>,
operation: HostedWorkspaceOperation,
request: PortableWorkspaceRequest,
): Promise<PortableWorkspaceReport> {
return invoker(hostedWorkspaceCommand(operation), { request });
}

export function invokePortableWorkspace(
Expand All @@ -37,14 +74,21 @@ export function invokePortableWorkspace(
) => Promise<PortableWorkspaceReport>,
request: PortableWorkspaceRequest,
): Promise<PortableWorkspaceReport> {
return invoker(workspaceWorkflowCommand("hosted"), { request });
return invokeHostedWorkspace(invoker, "attach", request);
}

export function invokeHostedWorkspaceList(
invoker: (command: string) => Promise<HostedWorkspaceListReport>,
): Promise<HostedWorkspaceListReport> {
return invoker("list_hosted_workspaces");
}

export function validatePortableWorkspaceForm(
form: PortableWorkspaceForm,
): PortableWorkspaceValidation {
const apiUrl = form.apiUrl;
const root = form.root;
const credentialRef = form.credentialRef;
const profileKey = form.profileKey;
if (!apiUrl) {
return { ok: false, message: "Enter the hosted workspace API URL." };
Expand Down Expand Up @@ -72,10 +116,13 @@ export function validatePortableWorkspaceForm(
if (!isAbsoluteWorkspaceRoot(root)) {
return { ok: false, message: "The local workspace root must be an absolute path." };
}
if (!/^hosted-workspace:[A-Za-z0-9_.:-]+$/.test(credentialRef) || credentialRef.length > 256) {
return { ok: false, message: "Enter a valid hosted-workspace credential reference." };
}
if (!/^[0-9a-f]{64}$/.test(profileKey)) {
return { ok: false, message: "The Workspace Profile key must be 64 lowercase hexadecimal characters." };
}
return { ok: true, request: { apiUrl, root, profileKey } };
return { ok: true, request: { apiUrl, root, credentialRef, profileKey } };
}

function isLoopbackHostname(hostname: string): boolean {
Expand All @@ -96,5 +143,5 @@ function isAbsoluteWorkspaceRoot(root: string): boolean {
}

export function portableWorkspaceSuccessMessage(report: PortableWorkspaceReport): string {
return `Materialized ${report.files} file(s) and ${report.directories} folder(s) at ${report.root}.`;
return `Attached ${report.files} file(s) and ${report.directories} folder(s) at ${report.root}.`;
}
Loading
Loading