feat: Add shared_network option for container workspaces
Add a new `shared_network` option to workspaces and workspace templates that controls network configuration for container (nspawn) workspaces: - When true (default): Share host network, bind-mount /etc/resolv.conf for DNS resolution - When false: Use isolated networking (--network-veth) for Tailscale or other custom network configurations This fixes DNS resolution issues in container workspaces that don't use Tailscale by properly sharing the host's DNS configuration. Changes: - Add shared_network field to Workspace and WorkspaceTemplate structs - Update nspawn command building to use shared_network setting - Add UI toggles in workspace template editor and workspace settings - Update API types and endpoints
This commit is contained in:
@@ -52,6 +52,7 @@ const buildSnapshot = (data: {
|
||||
skills: string[];
|
||||
envRows: EnvRow[];
|
||||
initScript: string;
|
||||
sharedNetwork: boolean | null;
|
||||
}) =>
|
||||
JSON.stringify({
|
||||
description: data.description,
|
||||
@@ -59,6 +60,7 @@ const buildSnapshot = (data: {
|
||||
skills: data.skills,
|
||||
env: data.envRows.map((row) => ({ key: row.key, value: row.value, encrypted: row.encrypted })),
|
||||
initScript: data.initScript,
|
||||
sharedNetwork: data.sharedNetwork,
|
||||
});
|
||||
|
||||
export default function WorkspaceTemplatesPage() {
|
||||
@@ -94,6 +96,7 @@ export default function WorkspaceTemplatesPage() {
|
||||
const [selectedSkills, setSelectedSkills] = useState<string[]>([]);
|
||||
const [envRows, setEnvRows] = useState<EnvRow[]>([]);
|
||||
const [initScript, setInitScript] = useState('');
|
||||
const [sharedNetwork, setSharedNetwork] = useState<boolean | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [dirty, setDirty] = useState(false);
|
||||
|
||||
@@ -118,8 +121,9 @@ export default function WorkspaceTemplatesPage() {
|
||||
skills: selectedSkills,
|
||||
envRows,
|
||||
initScript,
|
||||
sharedNetwork,
|
||||
}),
|
||||
[description, distro, selectedSkills, envRows, initScript]
|
||||
[description, distro, selectedSkills, envRows, initScript, sharedNetwork]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -186,12 +190,14 @@ export default function WorkspaceTemplatesPage() {
|
||||
const rows = toEnvRows(template.env_vars || {}, template.encrypted_keys);
|
||||
setEnvRows(rows);
|
||||
setInitScript(template.init_script || '');
|
||||
setSharedNetwork(template.shared_network ?? null);
|
||||
baselineRef.current = buildSnapshot({
|
||||
description: template.description || '',
|
||||
distro: template.distro || '',
|
||||
skills: template.skills || [],
|
||||
envRows: rows,
|
||||
initScript: template.init_script || '',
|
||||
sharedNetwork: template.shared_network ?? null,
|
||||
});
|
||||
setDirty(false);
|
||||
} catch (err) {
|
||||
@@ -210,6 +216,7 @@ export default function WorkspaceTemplatesPage() {
|
||||
env_vars: envRowsToMap(envRows),
|
||||
encrypted_keys: getEncryptedKeys(envRows),
|
||||
init_script: initScript,
|
||||
shared_network: sharedNetwork,
|
||||
});
|
||||
baselineRef.current = snapshot;
|
||||
setDirty(false);
|
||||
@@ -257,6 +264,7 @@ export default function WorkspaceTemplatesPage() {
|
||||
setSelectedSkills([]);
|
||||
setEnvRows([]);
|
||||
setInitScript('');
|
||||
setSharedNetwork(null);
|
||||
setDirty(false);
|
||||
await loadTemplates();
|
||||
} catch (err) {
|
||||
@@ -592,6 +600,51 @@ export default function WorkspaceTemplatesPage() {
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl bg-white/[0.02] border border-white/[0.05] p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<label className="text-xs text-white/40 block mb-1">Shared Network</label>
|
||||
<p className="text-[10px] text-white/25">
|
||||
Share host network and DNS. Disable for isolated networking (e.g., Tailscale).
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
// Toggle: null (default=true) -> false -> true -> null
|
||||
if (sharedNetwork === null) setSharedNetwork(false);
|
||||
else if (sharedNetwork === false) setSharedNetwork(true);
|
||||
else setSharedNetwork(null);
|
||||
}}
|
||||
className={cn(
|
||||
"relative w-11 h-6 rounded-full transition-colors",
|
||||
sharedNetwork === null
|
||||
? "bg-white/10" // default (true)
|
||||
: sharedNetwork
|
||||
? "bg-emerald-500/50"
|
||||
: "bg-red-500/30"
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"absolute top-1 w-4 h-4 rounded-full bg-white transition-all",
|
||||
sharedNetwork === null
|
||||
? "left-6" // default position (on)
|
||||
: sharedNetwork
|
||||
? "left-6"
|
||||
: "left-1"
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-[10px] text-white/30 mt-2">
|
||||
{sharedNetwork === null
|
||||
? "Default (enabled)"
|
||||
: sharedNetwork
|
||||
? "Enabled"
|
||||
: "Disabled (isolated)"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ export default function WorkspacesPage() {
|
||||
// Workspace settings state
|
||||
const [envRows, setEnvRows] = useState<EnvRow[]>([]);
|
||||
const [initScript, setInitScript] = useState('');
|
||||
const [sharedNetwork, setSharedNetwork] = useState<boolean | null>(null);
|
||||
const [savingWorkspace, setSavingWorkspace] = useState(false);
|
||||
const [savingTemplate, setSavingTemplate] = useState(false);
|
||||
const [templateName, setTemplateName] = useState('');
|
||||
@@ -162,6 +163,7 @@ export default function WorkspacesPage() {
|
||||
}
|
||||
setEnvRows(toEnvRows(selectedWorkspace.env_vars ?? {}));
|
||||
setInitScript(selectedWorkspace.init_script ?? '');
|
||||
setSharedNetwork(selectedWorkspace.shared_network ?? null);
|
||||
setSelectedSkills(selectedWorkspace.skills ?? []);
|
||||
setTemplateName(`${selectedWorkspace.name}-template`);
|
||||
setTemplateDescription('');
|
||||
@@ -341,6 +343,7 @@ export default function WorkspacesPage() {
|
||||
env_vars,
|
||||
init_script: initScript,
|
||||
skills: selectedSkills,
|
||||
shared_network: sharedNetwork,
|
||||
});
|
||||
setSelectedWorkspace(updated);
|
||||
await mutateWorkspaces();
|
||||
@@ -601,6 +604,51 @@ export default function WorkspacesPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Network settings for chroot workspaces */}
|
||||
{selectedWorkspace.workspace_type === 'chroot' && (
|
||||
<div className="rounded-lg bg-white/[0.02] border border-white/[0.05] p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs text-white/60 font-medium">Shared Network</p>
|
||||
<p className="text-[10px] text-white/30 mt-0.5">
|
||||
Share host network and DNS. Disable for isolated networking.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (sharedNetwork === null) setSharedNetwork(false);
|
||||
else if (sharedNetwork === false) setSharedNetwork(true);
|
||||
else setSharedNetwork(null);
|
||||
}}
|
||||
className={cn(
|
||||
"relative w-10 h-5 rounded-full transition-colors",
|
||||
sharedNetwork === null
|
||||
? "bg-white/10"
|
||||
: sharedNetwork
|
||||
? "bg-emerald-500/50"
|
||||
: "bg-red-500/30"
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"absolute top-0.5 w-4 h-4 rounded-full bg-white transition-all",
|
||||
sharedNetwork === null || sharedNetwork
|
||||
? "left-5"
|
||||
: "left-0.5"
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-[10px] text-white/25 mt-1.5">
|
||||
{sharedNetwork === null
|
||||
? "Default (enabled)"
|
||||
: sharedNetwork
|
||||
? "Enabled"
|
||||
: "Disabled (isolated)"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action hint for chroot workspaces */}
|
||||
{selectedWorkspace.workspace_type === 'chroot' && selectedWorkspace.status !== 'building' && selectedWorkspace.status !== 'ready' && (
|
||||
<div className="rounded-lg bg-amber-500/5 border border-amber-500/15 p-3">
|
||||
|
||||
@@ -1812,6 +1812,7 @@ export interface WorkspaceTemplate {
|
||||
env_vars: Record<string, string>;
|
||||
encrypted_keys: string[];
|
||||
init_script: string;
|
||||
shared_network?: boolean | null;
|
||||
}
|
||||
|
||||
export async function listWorkspaceTemplates(): Promise<WorkspaceTemplateSummary[]> {
|
||||
@@ -1835,6 +1836,7 @@ export async function saveWorkspaceTemplate(
|
||||
env_vars?: Record<string, string>;
|
||||
encrypted_keys?: string[];
|
||||
init_script?: string;
|
||||
shared_network?: boolean | null;
|
||||
}
|
||||
): Promise<void> {
|
||||
const res = await apiFetch(`/api/library/workspace-template/${encodeURIComponent(name)}`, {
|
||||
@@ -1863,6 +1865,7 @@ export async function renameWorkspaceTemplate(oldName: string, newName: string):
|
||||
env_vars: template.env_vars,
|
||||
encrypted_keys: template.encrypted_keys,
|
||||
init_script: template.init_script,
|
||||
shared_network: template.shared_network,
|
||||
});
|
||||
// Delete old template
|
||||
await deleteWorkspaceTemplate(oldName);
|
||||
@@ -1951,6 +1954,7 @@ export interface Workspace {
|
||||
distro?: string | null;
|
||||
env_vars: Record<string, string>;
|
||||
init_script?: string | null;
|
||||
shared_network?: boolean | null;
|
||||
}
|
||||
|
||||
// List workspaces
|
||||
@@ -1978,6 +1982,7 @@ export async function createWorkspace(data: {
|
||||
distro?: string;
|
||||
env_vars?: Record<string, string>;
|
||||
init_script?: string;
|
||||
shared_network?: boolean | null;
|
||||
}): Promise<Workspace> {
|
||||
const res = await apiFetch("/api/workspaces", {
|
||||
method: "POST",
|
||||
@@ -1999,6 +2004,7 @@ export async function updateWorkspace(
|
||||
distro?: string | null;
|
||||
env_vars?: Record<string, string>;
|
||||
init_script?: string | null;
|
||||
shared_network?: boolean | null;
|
||||
}
|
||||
): Promise<Workspace> {
|
||||
const res = await apiFetch(`/api/workspaces/${id}`, {
|
||||
|
||||
@@ -43,12 +43,12 @@ nav.nextra-nav-container {
|
||||
background: rgba(249, 250, 251, 0.5);
|
||||
backdrop-filter: blur(12px) saturate(150%);
|
||||
-webkit-backdrop-filter: blur(12px) saturate(150%);
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.06);
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.dark .nextra-sidebar-container {
|
||||
background: rgba(24, 23, 22, 0.5);
|
||||
border-right: 1px solid rgba(255, 248, 240, 0.06);
|
||||
border-right: 1px solid rgba(255, 248, 240, 0.1);
|
||||
}
|
||||
|
||||
/* Sidebar ambient tint - blue */
|
||||
@@ -861,36 +861,74 @@ aside a:hover,
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
SIDEBAR BOTTOM-LEFT CORNER FIX
|
||||
SIDEBAR FOOTER SEPARATOR
|
||||
============================================ */
|
||||
|
||||
/* Fix the rounded corner artifact in the bottom-left of the sidebar */
|
||||
/* Add top border to sidebar footer section (theme toggle, collapse button area) */
|
||||
.nextra-sidebar-container > div:last-child,
|
||||
aside.nextra-sidebar-container > div:last-child {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
||||
margin-top: auto;
|
||||
padding-top: 0.75rem;
|
||||
}
|
||||
|
||||
.dark .nextra-sidebar-container > div:last-child,
|
||||
.dark aside.nextra-sidebar-container > div:last-child {
|
||||
border-top: 1px solid rgba(255, 248, 240, 0.06);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
SIDEBAR COLLAPSED STATE & CORNER FIXES
|
||||
============================================ */
|
||||
|
||||
/* Remove all rounded corners from sidebar in any state */
|
||||
.nextra-sidebar-container,
|
||||
aside.nextra-sidebar-container,
|
||||
[class*="sidebar-container"],
|
||||
aside[class*="nextra"] {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
/* Target the theme switcher container at the bottom of the sidebar */
|
||||
.nextra-sidebar-container > div:last-child,
|
||||
aside > div:last-child,
|
||||
aside > div:last-of-type {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
/* Ensure the sidebar footer area has no rounded corners */
|
||||
[class*="sidebar"] [class*="footer"],
|
||||
[class*="sidebar"] > :last-child,
|
||||
aside [class*="theme"],
|
||||
aside [class*="toggle"] {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
/* Target any nested containers that might have rounded corners */
|
||||
aside[class*="nextra"],
|
||||
.nextra-sidebar-container *,
|
||||
aside.nextra-sidebar-container * {
|
||||
border-bottom-left-radius: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
/* Fix collapsed sidebar - extend to full height, remove floating box effect */
|
||||
.nextra-sidebar-container[data-collapsed="true"],
|
||||
aside[data-collapsed="true"],
|
||||
.nextra-sidebar-container:has(> div:only-child),
|
||||
[class*="sidebar"][class*="collapsed"] {
|
||||
background: rgba(249, 250, 251, 0.5) !important;
|
||||
backdrop-filter: blur(12px) saturate(150%) !important;
|
||||
-webkit-backdrop-filter: blur(12px) saturate(150%) !important;
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.1) !important;
|
||||
height: 100% !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.dark .nextra-sidebar-container[data-collapsed="true"],
|
||||
.dark aside[data-collapsed="true"],
|
||||
.dark .nextra-sidebar-container:has(> div:only-child),
|
||||
.dark [class*="sidebar"][class*="collapsed"] {
|
||||
background: rgba(24, 23, 22, 0.5) !important;
|
||||
border-right: 1px solid rgba(255, 248, 240, 0.1) !important;
|
||||
}
|
||||
|
||||
/* Remove background/border from inner containers in collapsed state to prevent floating box */
|
||||
.nextra-sidebar-container > div,
|
||||
aside.nextra-sidebar-container > div {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Keep the footer separator visible */
|
||||
.nextra-sidebar-container > div:last-child,
|
||||
aside.nextra-sidebar-container > div:last-child {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.06) !important;
|
||||
}
|
||||
|
||||
.dark .nextra-sidebar-container > div:last-child,
|
||||
.dark aside.nextra-sidebar-container > div:last-child {
|
||||
border-top: 1px solid rgba(255, 248, 240, 0.06) !important;
|
||||
}
|
||||
|
||||
/* Specifically remove the bottom-left radius from the sidebar wrapper */
|
||||
|
||||
@@ -294,6 +294,9 @@ pub struct SaveWorkspaceTemplateRequest {
|
||||
pub env_vars: Option<HashMap<String, String>>,
|
||||
pub encrypted_keys: Option<Vec<String>>,
|
||||
pub init_script: Option<String>,
|
||||
/// Whether to share the host network (default: true).
|
||||
/// Set to false for isolated networking (e.g., Tailscale).
|
||||
pub shared_network: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -958,6 +961,7 @@ async fn save_workspace_template(
|
||||
env_vars: req.env_vars.unwrap_or_default(),
|
||||
encrypted_keys: req.encrypted_keys.unwrap_or_default(),
|
||||
init_script: req.init_script.unwrap_or_default(),
|
||||
shared_network: req.shared_network,
|
||||
};
|
||||
|
||||
library
|
||||
|
||||
@@ -69,6 +69,9 @@ pub struct CreateWorkspaceRequest {
|
||||
pub env_vars: Option<HashMap<String, String>>,
|
||||
/// Init script to run when the workspace is built/rebuilt
|
||||
pub init_script: Option<String>,
|
||||
/// Whether to share the host network (default: true).
|
||||
/// Set to false for isolated networking (e.g., Tailscale).
|
||||
pub shared_network: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -89,6 +92,9 @@ pub struct UpdateWorkspaceRequest {
|
||||
pub env_vars: Option<HashMap<String, String>>,
|
||||
/// Init script to run when the workspace is built/rebuilt
|
||||
pub init_script: Option<String>,
|
||||
/// Whether to share the host network (default: true).
|
||||
/// Set to false for isolated networking (e.g., Tailscale).
|
||||
pub shared_network: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -107,6 +113,7 @@ pub struct WorkspaceResponse {
|
||||
pub distro: Option<String>,
|
||||
pub env_vars: HashMap<String, String>,
|
||||
pub init_script: Option<String>,
|
||||
pub shared_network: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<Workspace> for WorkspaceResponse {
|
||||
@@ -126,6 +133,7 @@ impl From<Workspace> for WorkspaceResponse {
|
||||
distro: w.distro,
|
||||
env_vars: w.env_vars,
|
||||
init_script: w.init_script,
|
||||
shared_network: w.shared_network,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -326,6 +334,11 @@ async fn create_workspace(
|
||||
None => None,
|
||||
};
|
||||
|
||||
// shared_network: request overrides template, default to true (None means true)
|
||||
let shared_network = req
|
||||
.shared_network
|
||||
.or_else(|| template_data.as_ref().and_then(|t| t.shared_network));
|
||||
|
||||
let mut workspace = match workspace_type {
|
||||
WorkspaceType::Host => Workspace {
|
||||
id: Uuid::new_v4(),
|
||||
@@ -343,6 +356,7 @@ async fn create_workspace(
|
||||
skills,
|
||||
tools: req.tools,
|
||||
plugins: req.plugins,
|
||||
shared_network,
|
||||
},
|
||||
WorkspaceType::Chroot => {
|
||||
let mut ws = Workspace::new_chroot(req.name, path);
|
||||
@@ -353,6 +367,7 @@ async fn create_workspace(
|
||||
ws.distro = distro;
|
||||
ws.env_vars = env_vars;
|
||||
ws.init_script = init_script;
|
||||
ws.shared_network = shared_network;
|
||||
ws
|
||||
}
|
||||
};
|
||||
@@ -520,6 +535,10 @@ async fn update_workspace(
|
||||
workspace.init_script = normalize_init_script(Some(init_script));
|
||||
}
|
||||
|
||||
if let Some(shared_network) = req.shared_network {
|
||||
workspace.shared_network = Some(shared_network);
|
||||
}
|
||||
|
||||
// Save the updated workspace
|
||||
state.workspaces.update(workspace.clone()).await;
|
||||
|
||||
|
||||
@@ -42,6 +42,9 @@ struct WorkspaceTemplateConfig {
|
||||
encrypted_keys: Vec<String>,
|
||||
#[serde(default)]
|
||||
init_script: String,
|
||||
/// Whether to share the host network (default: true).
|
||||
#[serde(default)]
|
||||
shared_network: Option<bool>,
|
||||
}
|
||||
|
||||
// Directory constants (OpenCode-aligned structure)
|
||||
@@ -1275,6 +1278,7 @@ impl LibraryStore {
|
||||
env_vars,
|
||||
encrypted_keys,
|
||||
init_script: config.init_script,
|
||||
shared_network: config.shared_network,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1329,6 +1333,7 @@ impl LibraryStore {
|
||||
env_vars,
|
||||
encrypted_keys: template.encrypted_keys.clone(),
|
||||
init_script: template.init_script.clone(),
|
||||
shared_network: template.shared_network,
|
||||
};
|
||||
|
||||
let content = serde_json::to_string_pretty(&config)?;
|
||||
|
||||
@@ -220,6 +220,11 @@ pub struct WorkspaceTemplate {
|
||||
/// Init script to run on build
|
||||
#[serde(default)]
|
||||
pub init_script: String,
|
||||
/// Whether to share the host network (default: true).
|
||||
/// When true, bind-mounts /etc/resolv.conf for DNS.
|
||||
/// Set to false for isolated networking (e.g., Tailscale).
|
||||
#[serde(default)]
|
||||
pub shared_network: Option<bool>,
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -119,6 +119,11 @@ pub struct Workspace {
|
||||
/// Plugin identifiers for hooks
|
||||
#[serde(default)]
|
||||
pub plugins: Vec<String>,
|
||||
/// Whether to share the host network (default: true).
|
||||
/// When true, bind-mounts /etc/resolv.conf for DNS.
|
||||
/// Set to false for isolated networking (e.g., Tailscale).
|
||||
#[serde(default)]
|
||||
pub shared_network: Option<bool>,
|
||||
}
|
||||
|
||||
impl Workspace {
|
||||
@@ -140,6 +145,7 @@ impl Workspace {
|
||||
skills: Vec::new(),
|
||||
tools: Vec::new(),
|
||||
plugins: Vec::new(),
|
||||
shared_network: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,6 +167,7 @@ impl Workspace {
|
||||
skills: Vec::new(),
|
||||
tools: Vec::new(),
|
||||
plugins: Vec::new(),
|
||||
shared_network: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -348,6 +355,7 @@ impl WorkspaceStore {
|
||||
skills: Vec::new(),
|
||||
tools: Vec::new(),
|
||||
plugins: Vec::new(),
|
||||
shared_network: None, // Default to shared network
|
||||
};
|
||||
|
||||
orphaned.push(workspace);
|
||||
@@ -509,6 +517,7 @@ fn opencode_entry_from_mcp(
|
||||
workspace_root: &Path,
|
||||
workspace_type: WorkspaceType,
|
||||
workspace_env: &HashMap<String, String>,
|
||||
shared_network: Option<bool>,
|
||||
) -> serde_json::Value {
|
||||
fn resolve_command_path(cmd: &str) -> String {
|
||||
let cmd_path = Path::new(cmd);
|
||||
@@ -638,7 +647,18 @@ fn opencode_entry_from_mcp(
|
||||
context_dir_name,
|
||||
);
|
||||
}
|
||||
cmd.extend(nspawn::tailscale_nspawn_extra_args(&merged_env));
|
||||
|
||||
// Network configuration based on shared_network setting:
|
||||
// - shared_network=true (default): Share host network, bind-mount /etc/resolv.conf for DNS
|
||||
// - shared_network=false: Isolated network (--network-veth), used with Tailscale
|
||||
let use_shared_network = shared_network.unwrap_or(true);
|
||||
if use_shared_network {
|
||||
// Bind-mount host's resolv.conf for DNS resolution in shared network mode
|
||||
cmd.push("--bind-ro=/etc/resolv.conf".to_string());
|
||||
} else {
|
||||
// Isolated network mode - use Tailscale network configuration
|
||||
cmd.extend(nspawn::tailscale_nspawn_extra_args(&merged_env));
|
||||
}
|
||||
for (key, value) in &nspawn_env {
|
||||
cmd.push(format!("--setenv={}={}", key, value));
|
||||
}
|
||||
@@ -666,6 +686,7 @@ async fn write_opencode_config(
|
||||
workspace_type: WorkspaceType,
|
||||
workspace_env: &HashMap<String, String>,
|
||||
skill_allowlist: Option<&[String]>,
|
||||
shared_network: Option<bool>,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut mcp_map = serde_json::Map::new();
|
||||
let mut used = std::collections::HashSet::new();
|
||||
@@ -688,6 +709,7 @@ async fn write_opencode_config(
|
||||
workspace_root,
|
||||
workspace_type,
|
||||
workspace_env,
|
||||
shared_network,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1243,6 +1265,7 @@ pub async fn prepare_custom_workspace(
|
||||
WorkspaceType::Host,
|
||||
&workspace_env,
|
||||
None,
|
||||
None, // shared_network: not relevant for host workspaces
|
||||
)
|
||||
.await?;
|
||||
Ok(workspace_dir)
|
||||
@@ -1279,6 +1302,7 @@ pub async fn prepare_mission_workspace_in(
|
||||
workspace.workspace_type,
|
||||
&workspace.env_vars,
|
||||
skill_allowlist,
|
||||
workspace.shared_network,
|
||||
)
|
||||
.await?;
|
||||
Ok(dir)
|
||||
@@ -1307,6 +1331,7 @@ pub async fn prepare_mission_workspace_with_skills(
|
||||
workspace.workspace_type,
|
||||
&workspace.env_vars,
|
||||
skill_allowlist,
|
||||
workspace.shared_network,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1407,6 +1432,7 @@ pub async fn prepare_task_workspace(
|
||||
WorkspaceType::Host,
|
||||
&workspace_env,
|
||||
None,
|
||||
None, // shared_network: not relevant for host workspaces
|
||||
)
|
||||
.await?;
|
||||
Ok(dir)
|
||||
@@ -1571,6 +1597,7 @@ pub async fn sync_all_workspaces(config: &Config, mcp: &McpRegistry) -> anyhow::
|
||||
WorkspaceType::Host,
|
||||
&workspace_env,
|
||||
None,
|
||||
None, // shared_network: not relevant for host workspaces
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
|
||||
Reference in New Issue
Block a user