mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 22:17:36 +00:00
Add sandbox Docker availability check, agent UUID resolver, and managed mode config redirects
Check Docker availability before enabling sandbox and gracefully disable with warning if unavailable. Add resolveAgentUUID helper in Telegram to support both agent keys and direct UUIDs. Expose sandbox config via environment variables for docker-compose overlay. Add compact redirect cards in config UI for Providers and Channels sections when in managed mode, directing users to dedicated management pages.
This commit is contained in:
1 parent
765bec2287
commit
1af9525a6f
4 files changed
+138
-17
No files matched your search
+10
-3
@@ -117,9 +117,16 @@ func runGateway() {
|
||||
// Sandbox manager (optional — routes tools through Docker containers)
|
||||
var sandboxMgr sandbox.Manager
|
||||
if sbCfg := cfg.Agents.Defaults.Sandbox; sbCfg != nil && sbCfg.Mode != "" && sbCfg.Mode != "off" {
|
||||
resolved := sbCfg.ToSandboxConfig()
|
||||
sandboxMgr = sandbox.NewDockerManager(resolved)
|
||||
slog.Info("sandbox enabled", "mode", string(resolved.Mode), "image", resolved.Image, "scope", string(resolved.Scope))
|
||||
if err := sandbox.CheckDockerAvailable(context.Background()); err != nil {
|
||||
slog.Warn("sandbox disabled: Docker not available",
|
||||
"configured_mode", sbCfg.Mode,
|
||||
"error", err,
|
||||
)
|
||||
} else {
|
||||
resolved := sbCfg.ToSandboxConfig()
|
||||
sandboxMgr = sandbox.NewDockerManager(resolved)
|
||||
slog.Info("sandbox enabled", "mode", string(resolved.Mode), "image", resolved.Image, "scope", string(resolved.Scope))
|
||||
}
|
||||
}
|
||||
|
||||
// Register file tools + exec tool (with sandbox routing via FsBridge if enabled)
|
||||
|
||||
@@ -14,6 +14,27 @@ import (
|
||||
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
||||
)
|
||||
|
||||
// resolveAgentUUID looks up the agent UUID from the channel's agent key.
|
||||
// Returns uuid.Nil if the agent key is empty or not found.
|
||||
func (c *Channel) resolveAgentUUID(ctx context.Context) (uuid.UUID, error) {
|
||||
key := c.AgentID()
|
||||
if key == "" {
|
||||
return uuid.Nil, fmt.Errorf("no agent key configured")
|
||||
}
|
||||
|
||||
// Try direct UUID parse first (future-proofing).
|
||||
if id, err := uuid.Parse(key); err == nil {
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// Look up by agent key.
|
||||
agent, err := c.agentStore.GetByKey(ctx, key)
|
||||
if err != nil {
|
||||
return uuid.Nil, fmt.Errorf("agent %q not found: %w", key, err)
|
||||
}
|
||||
return agent.ID, nil
|
||||
}
|
||||
|
||||
// handleBotCommand checks if the message is a known bot command and handles it.
|
||||
// Returns true if the message was handled as a command.
|
||||
func (c *Channel) handleBotCommand(ctx context.Context, message *telego.Message, chatID int64, chatIDStr, localKey, text, senderID string, isGroup, isForum bool, messageThreadID int) bool {
|
||||
@@ -127,8 +148,9 @@ func (c *Channel) handleWriterCommand(ctx context.Context, message *telego.Messa
|
||||
return
|
||||
}
|
||||
|
||||
agentID, err := uuid.Parse(c.AgentID())
|
||||
agentID, err := c.resolveAgentUUID(ctx)
|
||||
if err != nil {
|
||||
slog.Debug("writer command: agent resolve failed", "error", err)
|
||||
send("File writer management is not available (no agent).")
|
||||
return
|
||||
}
|
||||
@@ -150,7 +172,11 @@ func (c *Channel) handleWriterCommand(ctx context.Context, message *telego.Messa
|
||||
|
||||
// Extract target user from reply-to message
|
||||
if message.ReplyToMessage == nil || message.ReplyToMessage.From == nil {
|
||||
send(fmt.Sprintf("Reply to a message from the user you want to %s as a writer.", action))
|
||||
verb := "add"
|
||||
if action == "remove" {
|
||||
verb = "remove"
|
||||
}
|
||||
send(fmt.Sprintf("To %s a writer: find a message from that person, swipe to reply it, then type /%swriter.", verb, verb))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -206,8 +232,9 @@ func (c *Channel) handleListWriters(ctx context.Context, chatID int64, chatIDStr
|
||||
return
|
||||
}
|
||||
|
||||
agentID, err := uuid.Parse(c.AgentID())
|
||||
agentID, err := c.resolveAgentUUID(ctx)
|
||||
if err != nil {
|
||||
slog.Debug("list writers: agent resolve failed", "error", err)
|
||||
send("File writer management is not available (no agent).")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -167,6 +167,51 @@ func (c *Config) applyEnvOverrides() {
|
||||
envStr("GOCLAW_TSNET_HOSTNAME", &c.Tailscale.Hostname)
|
||||
envStr("GOCLAW_TSNET_AUTH_KEY", &c.Tailscale.AuthKey)
|
||||
envStr("GOCLAW_TSNET_DIR", &c.Tailscale.StateDir)
|
||||
|
||||
// Sandbox (for Docker-compose sandbox overlay)
|
||||
ensureSandbox := func() {
|
||||
if c.Agents.Defaults.Sandbox == nil {
|
||||
c.Agents.Defaults.Sandbox = &SandboxConfig{}
|
||||
}
|
||||
}
|
||||
if v := os.Getenv("GOCLAW_SANDBOX_MODE"); v != "" {
|
||||
ensureSandbox()
|
||||
c.Agents.Defaults.Sandbox.Mode = v
|
||||
}
|
||||
if v := os.Getenv("GOCLAW_SANDBOX_IMAGE"); v != "" {
|
||||
ensureSandbox()
|
||||
c.Agents.Defaults.Sandbox.Image = v
|
||||
}
|
||||
if v := os.Getenv("GOCLAW_SANDBOX_WORKSPACE_ACCESS"); v != "" {
|
||||
ensureSandbox()
|
||||
c.Agents.Defaults.Sandbox.WorkspaceAccess = v
|
||||
}
|
||||
if v := os.Getenv("GOCLAW_SANDBOX_SCOPE"); v != "" {
|
||||
ensureSandbox()
|
||||
c.Agents.Defaults.Sandbox.Scope = v
|
||||
}
|
||||
if v := os.Getenv("GOCLAW_SANDBOX_MEMORY_MB"); v != "" {
|
||||
ensureSandbox()
|
||||
if mb, err := strconv.Atoi(v); err == nil && mb > 0 {
|
||||
c.Agents.Defaults.Sandbox.MemoryMB = mb
|
||||
}
|
||||
}
|
||||
if v := os.Getenv("GOCLAW_SANDBOX_CPUS"); v != "" {
|
||||
ensureSandbox()
|
||||
if cpus, err := strconv.ParseFloat(v, 64); err == nil && cpus > 0 {
|
||||
c.Agents.Defaults.Sandbox.CPUs = cpus
|
||||
}
|
||||
}
|
||||
if v := os.Getenv("GOCLAW_SANDBOX_TIMEOUT_SEC"); v != "" {
|
||||
ensureSandbox()
|
||||
if sec, err := strconv.Atoi(v); err == nil && sec > 0 {
|
||||
c.Agents.Defaults.Sandbox.TimeoutSec = sec
|
||||
}
|
||||
}
|
||||
if v := os.Getenv("GOCLAW_SANDBOX_NETWORK"); v != "" {
|
||||
ensureSandbox()
|
||||
c.Agents.Defaults.Sandbox.NetworkEnabled = v == "true" || v == "1"
|
||||
}
|
||||
}
|
||||
|
||||
// applyContextPruningDefaults auto-enables context pruning when the Anthropic
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Settings, Save, RefreshCw, AlertCircle, ShieldAlert } from "lucide-react";
|
||||
import { Settings, Save, RefreshCw, AlertCircle, ShieldAlert, ArrowRight } from "lucide-react";
|
||||
import { Link } from "react-router";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Card, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { PageHeader } from "@/components/shared/page-header";
|
||||
import { EmptyState } from "@/components/shared/empty-state";
|
||||
import { DetailSkeleton } from "@/components/shared/loading-skeleton";
|
||||
import { useConfig } from "./hooks/use-config";
|
||||
import { useMinLoading } from "@/hooks/use-min-loading";
|
||||
import { ROUTES } from "@/lib/constants";
|
||||
import { GatewaySection } from "./sections/gateway-section";
|
||||
import { ProvidersSection } from "./sections/providers-section";
|
||||
import { AgentsDefaultsSection } from "./sections/agents-defaults-section";
|
||||
@@ -158,6 +161,27 @@ export function ConfigPage() {
|
||||
);
|
||||
}
|
||||
|
||||
/** Compact redirect card shown in managed mode for sections that have dedicated pages. */
|
||||
function ManagedRedirect({ title, description, to }: { title: string; description: string; to: string }) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-base">{title}</CardTitle>
|
||||
<CardDescription>{description}</CardDescription>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" className="gap-1.5 shrink-0" asChild>
|
||||
<Link to={to}>
|
||||
Manage <ArrowRight className="h-3.5 w-3.5" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
function ConfigUI({
|
||||
config,
|
||||
@@ -168,6 +192,8 @@ function ConfigUI({
|
||||
onPatch: (updates: Record<string, unknown>) => Promise<void>;
|
||||
saving: boolean;
|
||||
}) {
|
||||
const isManaged = (config.database as any)?.mode === "managed";
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<GatewaySection
|
||||
@@ -175,11 +201,19 @@ function ConfigUI({
|
||||
onSave={(v) => onPatch({ gateway: v })}
|
||||
saving={saving}
|
||||
/>
|
||||
<ProvidersSection
|
||||
data={config.providers as any}
|
||||
onSave={(v) => onPatch({ providers: v })}
|
||||
saving={saving}
|
||||
/>
|
||||
{isManaged ? (
|
||||
<ManagedRedirect
|
||||
title="LLM Providers"
|
||||
description="Managed via the Providers page in managed mode."
|
||||
to={ROUTES.PROVIDERS}
|
||||
/>
|
||||
) : (
|
||||
<ProvidersSection
|
||||
data={config.providers as any}
|
||||
onSave={(v) => onPatch({ providers: v })}
|
||||
saving={saving}
|
||||
/>
|
||||
)}
|
||||
<AgentsDefaultsSection
|
||||
data={config.agents as any}
|
||||
onSave={(v) => onPatch({ agents: v })}
|
||||
@@ -190,11 +224,19 @@ function ConfigUI({
|
||||
onSave={(v) => onPatch({ tools: v })}
|
||||
saving={saving}
|
||||
/>
|
||||
<ChannelsSection
|
||||
data={config.channels as any}
|
||||
onSave={(v) => onPatch({ channels: v })}
|
||||
saving={saving}
|
||||
/>
|
||||
{isManaged ? (
|
||||
<ManagedRedirect
|
||||
title="Channels"
|
||||
description="Managed via the Channels page in managed mode."
|
||||
to={ROUTES.CHANNELS}
|
||||
/>
|
||||
) : (
|
||||
<ChannelsSection
|
||||
data={config.channels as any}
|
||||
onSave={(v) => onPatch({ channels: v })}
|
||||
saving={saving}
|
||||
/>
|
||||
)}
|
||||
<SessionsSection
|
||||
data={config.sessions as any}
|
||||
onSave={(v) => onPatch({ sessions: v })}
|
||||
|
||||
Reference in new issue
Block a user