Skip to content

Commit dbf2c74

Browse files
authored
Merge pull request #145 from codeflash-ai/feature/workspace-materializer-v2
Materialize secure portable workspaces in loc and Desktop
2 parents 6a216c6 + 1eb2d54 commit dbf2c74

29 files changed

Lines changed: 11305 additions & 141 deletions

Cargo.lock

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/desktop/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,8 @@ starts the Notion broker OAuth flow, exposes the main daily-use screens, and
9696
opens a tray popover window. Remaining product gaps, especially workspace-level
9797
Notion mount creation and multi-file push orchestration, are tracked in
9898
`docs/deviations.md`.
99+
100+
The Hosted settings panel is an explicitly manual generation-2 materializer
101+
preview. It accepts an API origin, absolute destination, and Workspace Profile
102+
key for one invocation; it is not canonical Desktop source/mount integration
103+
and does not persist hosted credentials.

apps/desktop/src-tauri/src/main.rs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ use loc_cli::push::{
4747
PushOptions, PushReport, push_report_exit_code, run_push_with_daemon_at_state_root,
4848
};
4949
use loc_cli::restore::{RestoreOptions, run_restore};
50+
use loc_cli::sandbox::{
51+
SandboxContentEncodingPreference, SandboxInitOptions, SandboxInitReport, SandboxProfileKey,
52+
run_sandbox_init_with_profile_key, validate_sandbox_api_url,
53+
};
5054
use loc_cli::search::{
5155
SearchOptions, SearchResult, is_notion_url_host, notion_id_from_url,
5256
run_search_with_access_roots, source_url_host,
@@ -453,6 +457,14 @@ struct WorkspaceMountOnboardingRequest {
453457
action: String,
454458
}
455459

460+
#[derive(Clone, Deserialize)]
461+
#[serde(rename_all = "camelCase")]
462+
struct PortableWorkspaceMaterializationRequest {
463+
api_url: String,
464+
root: String,
465+
profile_key: String,
466+
}
467+
456468
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
457469
enum WorkspaceMountOnboardingAction {
458470
Start,
@@ -1429,6 +1441,43 @@ async fn create_workspace_mount(app: AppHandle, path: String) -> ActionReport {
14291441
.await
14301442
}
14311443

1444+
#[tauri::command]
1445+
async fn materialize_portable_workspace(
1446+
request: PortableWorkspaceMaterializationRequest,
1447+
) -> Result<SandboxInitReport, String> {
1448+
let (options, profile_key) = portable_workspace_options(request)?;
1449+
tauri::async_runtime::spawn_blocking(move || {
1450+
run_sandbox_init_with_profile_key(
1451+
options,
1452+
profile_key,
1453+
SandboxContentEncodingPreference::Automatic,
1454+
)
1455+
.map_err(|error| format!("Portable workspace materialization failed: {error}"))
1456+
})
1457+
.await
1458+
.map_err(|error| format!("Portable workspace worker failed: {error}"))?
1459+
}
1460+
1461+
fn portable_workspace_options(
1462+
request: PortableWorkspaceMaterializationRequest,
1463+
) -> Result<(SandboxInitOptions, SandboxProfileKey), String> {
1464+
validate_sandbox_api_url(&request.api_url)
1465+
.map_err(|error| format!("Portable workspace API URL is invalid: {error}"))?;
1466+
let root = PathBuf::from(request.root);
1467+
if !root.is_absolute() {
1468+
return Err("Portable workspace root must be an absolute path".to_string());
1469+
}
1470+
let profile_key = SandboxProfileKey::new(request.profile_key)
1471+
.map_err(|error| format!("Portable workspace credential is invalid: {error}"))?;
1472+
Ok((
1473+
SandboxInitOptions {
1474+
api_url: request.api_url,
1475+
root,
1476+
},
1477+
profile_key,
1478+
))
1479+
}
1480+
14321481
#[tauri::command]
14331482
async fn create_desktop_mount(app: AppHandle, request: CreateDesktopMountRequest) -> ActionReport {
14341483
create_desktop_mount_command(app, request).await
@@ -12083,6 +12132,54 @@ mod tests {
1208312132
assert!(super::absolute_state_root(PathBuf::from(".loc")).is_absolute());
1208412133
}
1208512134

12135+
#[test]
12136+
fn portable_workspace_invoke_boundary_rejects_invalid_urls_and_relative_roots() {
12137+
for api_url in [
12138+
"https://workspace.example.test/api",
12139+
"https://user@workspace.example.test",
12140+
"https://workspace.example.test?tenant=7",
12141+
"https://workspace.example.test#fragment",
12142+
"http://workspace.example.test",
12143+
] {
12144+
let error =
12145+
super::portable_workspace_options(super::PortableWorkspaceMaterializationRequest {
12146+
api_url: api_url.to_string(),
12147+
root: std::env::temp_dir().join("Locality").display().to_string(),
12148+
profile_key: "a".repeat(64),
12149+
})
12150+
.expect_err("invalid Desktop API URL must fail before invocation");
12151+
assert!(error.contains("API URL is invalid"), "{api_url}: {error}");
12152+
}
12153+
12154+
let error =
12155+
super::portable_workspace_options(super::PortableWorkspaceMaterializationRequest {
12156+
api_url: "https://workspace.example.test".to_string(),
12157+
root: "relative/Locality".to_string(),
12158+
profile_key: "a".repeat(64),
12159+
})
12160+
.expect_err("relative Desktop root must fail before invocation");
12161+
assert!(error.contains("absolute path"));
12162+
}
12163+
12164+
#[test]
12165+
fn portable_workspace_invoke_boundary_accepts_https_and_loopback_http() {
12166+
for api_url in [
12167+
"https://workspace.example.test",
12168+
"http://127.0.0.1:8080",
12169+
"http://127.1:8080",
12170+
] {
12171+
let (options, _) =
12172+
super::portable_workspace_options(super::PortableWorkspaceMaterializationRequest {
12173+
api_url: api_url.to_string(),
12174+
root: std::env::temp_dir().join("Locality").display().to_string(),
12175+
profile_key: "a".repeat(64),
12176+
})
12177+
.expect("valid Desktop workspace request");
12178+
assert_eq!(options.api_url, api_url);
12179+
assert!(options.root.is_absolute());
12180+
}
12181+
}
12182+
1208612183
#[cfg(unix)]
1208712184
#[test]
1208812185
fn write_file_atomic_falls_back_to_existing_file_overwrite() {
@@ -18106,6 +18203,7 @@ fn main() {
1810618203
ensure_runtime_ready,
1810718204
ensure_terminal_cli_available,
1810818205
create_workspace_mount,
18206+
materialize_portable_workspace,
1810918207
create_desktop_mount,
1811018208
connect_granola,
1811118209
connect_linear,

apps/desktop/src/App.tsx

Lines changed: 120 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,13 @@ import {
110110
type SourceConnectorId,
111111
type SourceSetupState,
112112
} from "./source-setup";
113+
import {
114+
invokePortableWorkspace,
115+
portableWorkspaceSuccessMessage,
116+
validatePortableWorkspaceForm,
117+
workspaceWorkflowCommand,
118+
type PortableWorkspaceReport,
119+
} from "./portable-workspace";
113120
import gmailIconUrl from "./assets/connectors/gmail.svg";
114121
import googleCalendarIconUrl from "./assets/connectors/google-calendar.svg";
115122
import googleDocsIconUrl from "./assets/connectors/google-docs.svg";
@@ -131,7 +138,7 @@ type OnboardingConnectorId = SourceConnectorId;
131138
type ReviewFilter = "all" | "approvals" | "problems";
132139
type FileStatusFilter = "all" | "review" | "conflict" | "synced";
133140
type DestructiveSettingsAction = "reset" | "uninstall";
134-
type SettingsSection = "general" | "sources" | "sync" | "activity" | "agents" | "advanced" | "about";
141+
type SettingsSection = "general" | "sources" | "hosted" | "sync" | "activity" | "agents" | "advanced" | "about";
135142
type SourceListViewMode = "list" | "tiles";
136143
type AppTheme = "system" | "light" | "dark";
137144

@@ -3108,7 +3115,7 @@ function HomeView({
31083115
async function createMount() {
31093116
setActionError("");
31103117
const report = await callCommand<ActionReport>(
3111-
"create_workspace_mount",
3118+
workspaceWorkflowCommand("local"),
31123119
{ path: snapshot.mount.localPath },
31133120
{ ok: true, message: "Created demo mount." },
31143121
);
@@ -5954,6 +5961,13 @@ function SettingsView({
59545961
const [settingsSection, setSettingsSection] = useState<SettingsSection>("general");
59555962
const [busySetting, setBusySetting] = useState("");
59565963
const [localSettings, setLocalSettings] = useState(snapshot.settings);
5964+
const [portableApiUrl, setPortableApiUrl] = useState("");
5965+
const [portableRoot, setPortableRoot] = useState("");
5966+
const [portableProfileKey, setPortableProfileKey] = useState("");
5967+
const [portableWorkspaceState, setPortableWorkspaceState] = useState<
5968+
"idle" | "materializing" | "success" | "error"
5969+
>("idle");
5970+
const [portableWorkspaceMessage, setPortableWorkspaceMessage] = useState("");
59575971
const daemonStopped = snapshot.health.state === "stopped";
59585972
const runtimeStopped = snapshot.health.state === "runtime_stopped";
59595973
const runtimeNeedsRepair = daemonStopped || runtimeStopped;
@@ -6131,9 +6145,51 @@ function SettingsView({
61316145
}
61326146
}
61336147

6148+
async function materializePortableWorkspace() {
6149+
if (portableWorkspaceState === "materializing") {
6150+
return;
6151+
}
6152+
const validation = validatePortableWorkspaceForm({
6153+
apiUrl: portableApiUrl,
6154+
root: portableRoot,
6155+
profileKey: portableProfileKey,
6156+
});
6157+
if (!validation.ok) {
6158+
setPortableWorkspaceState("error");
6159+
setPortableWorkspaceMessage(validation.message);
6160+
return;
6161+
}
6162+
setPortableWorkspaceState("materializing");
6163+
setPortableWorkspaceMessage("Negotiating and materializing the hosted workspace…");
6164+
try {
6165+
const fallback: PortableWorkspaceReport = {
6166+
ok: true,
6167+
root: validation.request.root,
6168+
session_id: "demo-session",
6169+
content_encoding: "identity",
6170+
entries: 0,
6171+
files: 0,
6172+
directories: 0,
6173+
materialized_bytes: 0,
6174+
decoded_bytes: 0,
6175+
};
6176+
const report = await invokePortableWorkspace(
6177+
(command, args) => callCommand<PortableWorkspaceReport>(command, args, fallback),
6178+
validation.request,
6179+
);
6180+
setPortableProfileKey("");
6181+
setPortableWorkspaceState("success");
6182+
setPortableWorkspaceMessage(portableWorkspaceSuccessMessage(report));
6183+
} catch (error) {
6184+
setPortableWorkspaceState("error");
6185+
setPortableWorkspaceMessage(errorMessage(error));
6186+
}
6187+
}
6188+
61346189
const settingsSections: Array<{ id: SettingsSection; label: string; description: string }> = [
61356190
{ id: "general", label: "General", description: "Startup and desktop behavior" },
61366191
{ id: "sources", label: "Sources", description: "Connected workspaces and local folders" },
6192+
{ id: "hosted", label: "Hosted (preview)", description: "Manual portable materializer" },
61376193
{ id: "sync", label: "Sync", description: "Live Mode and review policy" },
61386194
{ id: "activity", label: "Activity", description: "Recent events and debug queue" },
61396195
{ id: "agents", label: "Agents", description: "Local agent instructions" },
@@ -6217,6 +6273,68 @@ function SettingsView({
62176273
</section>
62186274
)}
62196275

6276+
{settingsSection === "hosted" && (
6277+
<section className="panel settings-section-panel portable-workspace-panel">
6278+
<PanelTitle title="Hosted materializer preview" />
6279+
<p className="quiet-note">
6280+
Manually materialize or recover a generation-2 workspace. This preview is not yet bound to Desktop sources, mounts, or stored credentials.
6281+
</p>
6282+
<div className="portable-workspace-fields">
6283+
<label className="source-inline-field">
6284+
<span>Workspace API URL</span>
6285+
<input
6286+
type="url"
6287+
value={portableApiUrl}
6288+
placeholder="https://workspace.example.com"
6289+
disabled={portableWorkspaceState === "materializing"}
6290+
onChange={(event) => setPortableApiUrl(event.target.value)}
6291+
/>
6292+
</label>
6293+
<label className="source-inline-field">
6294+
<span>Local workspace root</span>
6295+
<input
6296+
value={portableRoot}
6297+
placeholder="/mnt/locality"
6298+
disabled={portableWorkspaceState === "materializing"}
6299+
onChange={(event) => setPortableRoot(event.target.value)}
6300+
/>
6301+
</label>
6302+
<label className="source-inline-field portable-workspace-key-field">
6303+
<span>Workspace Profile key</span>
6304+
<input
6305+
type="password"
6306+
autoComplete="off"
6307+
value={portableProfileKey}
6308+
placeholder="64-character key"
6309+
disabled={portableWorkspaceState === "materializing"}
6310+
onChange={(event) => setPortableProfileKey(event.target.value)}
6311+
onKeyDown={(event) => {
6312+
if (event.key === "Enter") {
6313+
void materializePortableWorkspace();
6314+
}
6315+
}}
6316+
/>
6317+
</label>
6318+
</div>
6319+
<PrimaryButton
6320+
compact
6321+
icon={portableWorkspaceState === "materializing" ? <Loader2 className="spin-icon" /> : <Download />}
6322+
disabled={portableWorkspaceState === "materializing"}
6323+
onClick={() => void materializePortableWorkspace()}
6324+
>
6325+
{portableWorkspaceState === "materializing" ? "Materializing" : "Materialize Workspace"}
6326+
</PrimaryButton>
6327+
{portableWorkspaceMessage && (
6328+
<p
6329+
className={portableWorkspaceState === "error" ? "field-error" : "quiet-note inline-note"}
6330+
role={portableWorkspaceState === "error" ? "alert" : "status"}
6331+
>
6332+
{portableWorkspaceMessage}
6333+
</p>
6334+
)}
6335+
</section>
6336+
)}
6337+
62206338
{settingsSection === "sync" && (
62216339
<section className="panel settings-section-panel">
62226340
<PanelTitle title="Sync and review policy" />

0 commit comments

Comments
 (0)