Compare commits

..

1 Commits

Author SHA1 Message Date
Jiayuan Zhang
be8a2040f5 fix(cli): always use localhost for auth callback in browser login
When the app URL pointed to a remote private IP (e.g. 192.168.11.200),
the CLI incorrectly used that IP as the callback host. The callback
HTTP server runs on the CLI's local machine, but the browser redirect
would target the remote server's IP on a random port that isn't open
there — breaking the entire auth flow for remote self-hosted setups.

Since openBrowser() always opens the browser on the same machine as the
CLI, the callback must always target localhost.

Closes #1056
2026-04-15 14:25:28 +08:00
29 changed files with 84 additions and 495 deletions

View File

@@ -7,8 +7,10 @@ DATABASE_URL=postgres://multica:multica@localhost:5432/multica?sslmode=disable
# Server
PORT=8080
APP_ENV=
TASK_DOMAIN=localhost
JWT_SECRET=change-me-in-production
MULTICA_SERVER_URL=ws://localhost:8080/ws
MULTICA_SERVER_URL=http://localhost:8080
MULTICA_APP_URL=http://localhost:3000
MULTICA_DAEMON_CONFIG=
MULTICA_WORKSPACE_ID=
@@ -55,11 +57,9 @@ ALLOWED_ORIGINS=
# Frontend
FRONTEND_PORT=3000
FRONTEND_ORIGIN=http://localhost:3000
# Leave empty — auto-derived from page origin in browser, set by Makefile for local dev.
# Only set explicitly if frontend and backend are on different domains.
NEXT_PUBLIC_API_URL=
NEXT_PUBLIC_WS_URL=
NEXT_PUBLIC_API_URL=http://localhost:8080
NEXT_PUBLIC_WS_URL=ws://localhost:8080/ws
# Remote API (optional) — set to proxy local frontend to a remote backend
# Leave empty to use local backend (localhost:8080)
# REMOTE_API_URL=https://multica-api.copilothub.ai
REMOTE_API_URL=http://localhost:8080

15
Caddyfile Normal file
View File

@@ -0,0 +1,15 @@
{$TASK_DOMAIN} {
@next_static path /_next/static/*
header @next_static Cache-Control "public, max-age=31536000, immutable, no-transform"
reverse_proxy /api/* backend:8080
reverse_proxy /auth/send-code backend:8080
reverse_proxy /auth/verify-code backend:8080
reverse_proxy /auth/google backend:8080
reverse_proxy /auth/logout backend:8080
reverse_proxy /ws backend:8080
reverse_proxy /ws/* backend:8080
reverse_proxy /health backend:8080
reverse_proxy /health/* backend:8080
reverse_proxy frontend:3000
}

View File

@@ -37,9 +37,11 @@ RUN pnpm install --frozen-lockfile --offline
# Set build-time env: tells Next.js rewrites to proxy API calls to the backend service
ARG REMOTE_API_URL=http://backend:8080
ARG NEXT_PUBLIC_GOOGLE_CLIENT_ID
ARG NEXT_PUBLIC_API_URL
ARG NEXT_PUBLIC_WS_URL
ENV REMOTE_API_URL=$REMOTE_API_URL
ENV NEXT_PUBLIC_GOOGLE_CLIENT_ID=$NEXT_PUBLIC_GOOGLE_CLIENT_ID
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_WS_URL=$NEXT_PUBLIC_WS_URL
ENV STANDALONE=true

View File

@@ -315,6 +315,8 @@ api.example.com {
}
```
For a single-domain setup, route the frontend and backend through one hostname and forward `/api`, `/auth`, `/ws`, and `/health` to the backend while sending everything else to the frontend. This repository now includes a root `Caddyfile` and `docker-compose.selfhost.yml` service for that pattern.
### Nginx
```nginx

View File

@@ -55,7 +55,6 @@ export const mockAgents: Agent[] = [
runtime_mode: "cloud",
runtime_config: {},
custom_env: {},
custom_args: [],
custom_env_redacted: false,
visibility: "workspace",
max_concurrent_tasks: 3,

View File

@@ -5,8 +5,8 @@
# # Edit .env — change JWT_SECRET at minimum
# docker compose -f docker-compose.selfhost.yml up -d
#
# Frontend: http://localhost:3000
# Backend: http://localhost:8080 (also used by CLI/daemon)
# Frontend: https://$TASK_DOMAIN (via Caddy reverse proxy)
# Backend: internal on backend:8080 (health exposed at /health through Caddy)
name: multica
@@ -35,12 +35,13 @@ services:
postgres:
condition: service_healthy
ports:
- "${PORT:-8080}:8080"
- "127.0.0.1:${PORT:-8080}:8080"
environment:
DATABASE_URL: postgres://${POSTGRES_USER:-multica}:${POSTGRES_PASSWORD:-multica}@postgres:5432/${POSTGRES_DB:-multica}?sslmode=disable
APP_ENV: ${APP_ENV:-}
PORT: "8080"
JWT_SECRET: ${JWT_SECRET:-change-me-in-production}
FRONTEND_ORIGIN: ${FRONTEND_ORIGIN:-http://localhost:3000}
FRONTEND_ORIGIN: ${FRONTEND_ORIGIN:-https://${TASK_DOMAIN:-localhost}}
CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS:-}
RESEND_API_KEY: ${RESEND_API_KEY:-}
RESEND_FROM_EMAIL: ${RESEND_FROM_EMAIL:-noreply@multica.ai}
@@ -60,15 +61,35 @@ services:
context: .
dockerfile: Dockerfile.web
args:
REMOTE_API_URL: http://backend:8080
REMOTE_API_URL: ${REMOTE_API_URL:-http://backend:8080}
NEXT_PUBLIC_GOOGLE_CLIENT_ID: ${NEXT_PUBLIC_GOOGLE_CLIENT_ID:-}
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-}
NEXT_PUBLIC_WS_URL: ${NEXT_PUBLIC_WS_URL:-}
depends_on:
- backend
ports:
- "${FRONTEND_PORT:-3000}:3000"
- "127.0.0.1:${FRONTEND_PORT:-3000}:3000"
environment:
HOSTNAME: "0.0.0.0"
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-}
NEXT_PUBLIC_WS_URL: ${NEXT_PUBLIC_WS_URL:-}
caddy:
image: caddy:2-alpine
depends_on:
- frontend
- backend
ports:
- "80:80"
- "443:443"
environment:
TASK_DOMAIN: ${TASK_DOMAIN:-localhost}
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
volumes:
pgdata:
caddy_data:
caddy_config:

View File

@@ -48,7 +48,6 @@ export interface Agent {
runtime_mode: AgentRuntimeMode;
runtime_config: Record<string, unknown>;
custom_env: Record<string, string>;
custom_args: string[];
custom_env_redacted: boolean;
visibility: AgentVisibility;
status: AgentStatus;
@@ -69,7 +68,6 @@ export interface CreateAgentRequest {
runtime_id: string;
runtime_config?: Record<string, unknown>;
custom_env?: Record<string, string>;
custom_args?: string[];
visibility?: AgentVisibility;
max_concurrent_tasks?: number;
}
@@ -82,7 +80,6 @@ export interface UpdateAgentRequest {
runtime_id?: string;
runtime_config?: Record<string, unknown>;
custom_env?: Record<string, string>;
custom_args?: string[];
visibility?: AgentVisibility;
status?: AgentStatus;
max_concurrent_tasks?: number;

View File

@@ -12,7 +12,6 @@ import {
MoreHorizontal,
Settings,
KeyRound,
Terminal,
} from "lucide-react";
import type { Agent, RuntimeDevice, MemberWithUser } from "@multica/core/types";
import {
@@ -37,20 +36,18 @@ import { SkillsTab } from "./tabs/skills-tab";
import { TasksTab } from "./tabs/tasks-tab";
import { SettingsTab } from "./tabs/settings-tab";
import { EnvTab } from "./tabs/env-tab";
import { CustomArgsTab } from "./tabs/custom-args-tab";
function getRuntimeDevice(agent: Agent, runtimes: RuntimeDevice[]): RuntimeDevice | undefined {
return runtimes.find((runtime) => runtime.id === agent.runtime_id);
}
type DetailTab = "instructions" | "skills" | "tasks" | "env" | "custom_args" | "settings";
type DetailTab = "instructions" | "skills" | "tasks" | "env" | "settings";
const detailTabs: { id: DetailTab; label: string; icon: typeof FileText }[] = [
{ id: "instructions", label: "Instructions", icon: FileText },
{ id: "skills", label: "Skills", icon: BookOpenText },
{ id: "tasks", label: "Tasks", icon: ListTodo },
{ id: "env", label: "Environment", icon: KeyRound },
{ id: "custom_args", label: "Custom Args", icon: Terminal },
{ id: "settings", label: "Settings", icon: Settings },
];
@@ -175,12 +172,6 @@ export function AgentDetail({
onSave={(updates) => onUpdate(agent.id, updates)}
/>
)}
{activeTab === "custom_args" && (
<CustomArgsTab
agent={agent}
onSave={(updates) => onUpdate(agent.id, updates)}
/>
)}
{activeTab === "settings" && (
<SettingsTab
agent={agent}

View File

@@ -1,126 +0,0 @@
"use client";
import { useState } from "react";
import {
Loader2,
Save,
Plus,
Trash2,
} from "lucide-react";
import type { Agent } from "@multica/core/types";
import { Button } from "@multica/ui/components/ui/button";
import { Input } from "@multica/ui/components/ui/input";
import { Label } from "@multica/ui/components/ui/label";
import { toast } from "sonner";
interface ArgEntry {
id: string;
value: string;
}
function argsToEntries(args: string[]): ArgEntry[] {
return args.map((value) => ({ id: crypto.randomUUID(), value }));
}
function entriesToArgs(entries: ArgEntry[]): string[] {
return entries.flatMap((e) => e.value.trim().split(/\s+/)).filter(Boolean);
}
export function CustomArgsTab({
agent,
onSave,
}: {
agent: Agent;
onSave: (updates: Partial<Agent>) => Promise<void>;
}) {
const [entries, setEntries] = useState<ArgEntry[]>(
argsToEntries(agent.custom_args ?? []),
);
const [saving, setSaving] = useState(false);
const currentArgs = entriesToArgs(entries);
const originalArgs = agent.custom_args ?? [];
const dirty = JSON.stringify(currentArgs) !== JSON.stringify(originalArgs);
const addEntry = () => {
setEntries([...entries, { id: crypto.randomUUID(), value: "" }]);
};
const removeEntry = (index: number) => {
setEntries(entries.filter((_, i) => i !== index));
};
const updateEntry = (index: number, value: string) => {
setEntries(
entries.map((entry, i) => (i === index ? { ...entry, value } : entry)),
);
};
const handleSave = async () => {
setSaving(true);
try {
await onSave({ custom_args: currentArgs });
toast.success("Custom arguments saved");
} catch {
toast.error("Failed to save custom arguments");
} finally {
setSaving(false);
}
};
return (
<div className="max-w-lg space-y-4">
<div className="flex items-center justify-between">
<div>
<Label className="text-xs text-muted-foreground">
Custom Arguments
</Label>
<p className="text-xs text-muted-foreground mt-0.5">
Additional CLI arguments appended to the agent command at launch
(e.g. --model claude-sonnet-4-20250514)
</p>
</div>
<Button
type="button"
variant="outline"
size="sm"
onClick={addEntry}
className="h-7 gap-1 text-xs"
>
<Plus className="h-3 w-3" />
Add
</Button>
</div>
{entries.length > 0 && (
<div className="space-y-2">
{entries.map((entry, index) => (
<div key={entry.id} className="flex items-center gap-2">
<Input
value={entry.value}
onChange={(e) => updateEntry(index, e.target.value)}
placeholder="--model claude-sonnet-4-20250514"
className="flex-1 font-mono text-xs"
/>
<button
type="button"
onClick={() => removeEntry(index)}
className="shrink-0 text-muted-foreground hover:text-destructive"
>
<Trash2 className="h-3.5 w-3.5" />
</button>
</div>
))}
</div>
)}
<Button onClick={handleSave} disabled={!dirty || saving} size="sm">
{saving ? (
<Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />
) : (
<Save className="h-3.5 w-3.5 mr-1.5" />
)}
Save
</Button>
</div>
);
}

View File

@@ -114,7 +114,6 @@ func init() {
agentCreateCmd.Flags().String("instructions", "", "Agent instructions")
agentCreateCmd.Flags().String("runtime-id", "", "Runtime ID (required)")
agentCreateCmd.Flags().String("runtime-config", "", "Runtime config as JSON string")
agentCreateCmd.Flags().String("custom-args", "", "Custom CLI arguments as JSON array (e.g. '[\"--model\", \"o3\"]')")
agentCreateCmd.Flags().String("visibility", "private", "Visibility: private or workspace")
agentCreateCmd.Flags().Int32("max-concurrent-tasks", 6, "Maximum concurrent tasks")
agentCreateCmd.Flags().String("output", "json", "Output format: table or json")
@@ -125,7 +124,6 @@ func init() {
agentUpdateCmd.Flags().String("instructions", "", "New instructions")
agentUpdateCmd.Flags().String("runtime-id", "", "New runtime ID")
agentUpdateCmd.Flags().String("runtime-config", "", "New runtime config as JSON string")
agentUpdateCmd.Flags().String("custom-args", "", "New custom CLI arguments as JSON array (e.g. '[\"--model\", \"o3\"]')")
agentUpdateCmd.Flags().String("visibility", "", "New visibility: private or workspace")
agentUpdateCmd.Flags().String("status", "", "New status")
agentUpdateCmd.Flags().Int32("max-concurrent-tasks", 0, "New max concurrent tasks")
@@ -339,14 +337,6 @@ func runAgentCreate(cmd *cobra.Command, _ []string) error {
}
body["runtime_config"] = rc
}
if cmd.Flags().Changed("custom-args") {
v, _ := cmd.Flags().GetString("custom-args")
var ca []string
if err := json.Unmarshal([]byte(v), &ca); err != nil {
return fmt.Errorf("--custom-args must be a valid JSON array: %w", err)
}
body["custom_args"] = ca
}
if cmd.Flags().Changed("visibility") {
v, _ := cmd.Flags().GetString("visibility")
body["visibility"] = v
@@ -404,14 +394,6 @@ func runAgentUpdate(cmd *cobra.Command, args []string) error {
}
body["runtime_config"] = rc
}
if cmd.Flags().Changed("custom-args") {
v, _ := cmd.Flags().GetString("custom-args")
var ca []string
if err := json.Unmarshal([]byte(v), &ca); err != nil {
return fmt.Errorf("--custom-args must be a valid JSON array: %w", err)
}
body["custom_args"] = ca
}
if cmd.Flags().Changed("visibility") {
v, _ := cmd.Flags().GetString("visibility")
body["visibility"] = v
@@ -426,7 +408,7 @@ func runAgentUpdate(cmd *cobra.Command, args []string) error {
}
if len(body) == 0 {
return fmt.Errorf("no fields to update; use --name, --description, --instructions, --runtime-id, --runtime-config, --custom-args, --visibility, --status, or --max-concurrent-tasks")
return fmt.Errorf("no fields to update; use --name, --description, --instructions, --runtime-id, --runtime-config, --visibility, --status, or --max-concurrent-tasks")
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)

View File

@@ -98,21 +98,12 @@ func runAuthLoginBrowser(cmd *cobra.Command) error {
serverURL := resolveServerURL(cmd)
appURL := resolveAppURL(cmd)
// Determine the callback host from the configured app URL.
// For self-hosted setups where the browser is on a different machine
// (e.g. Multica running on a LAN server), use the server's private IP
// so the browser can reach the CLI's local HTTP server.
// For production (public hostnames like multica.ai), keep localhost —
// the browser and CLI are on the same machine.
// The callback always targets localhost because the browser is opened on
// the same machine as the CLI (via openBrowser). Even when the Multica
// server is on a remote LAN host, the browser-side redirect must reach
// the CLI's local HTTP server, not the remote server.
callbackHost := "localhost"
bindAddr := "127.0.0.1"
if parsed, err := url.Parse(appURL); err == nil {
h := parsed.Hostname()
if ip := net.ParseIP(h); ip != nil && ip.IsPrivate() {
callbackHost = h
bindAddr = "0.0.0.0"
}
}
// Start a local HTTP server on a random port to receive the callback.
listener, err := net.Listen("tcp", bindAddr+":0")

View File

@@ -916,16 +916,11 @@ func (d *Daemon) runTask(ctx context.Context, task Task, provider string, taskLo
taskStart := time.Now()
var customArgs []string
if task.Agent != nil {
customArgs = task.Agent.CustomArgs
}
execOpts := agent.ExecOptions{
Cwd: env.WorkDir,
Model: entry.Model,
Timeout: d.cfg.AgentTimeout,
ResumeSessionID: task.PriorSessionID,
CustomArgs: customArgs,
}
result, tools, err := d.executeAndDrain(ctx, backend, prompt, execOpts, taskLog, task.ID)

View File

@@ -45,7 +45,6 @@ type AgentData struct {
Instructions string `json:"instructions"`
Skills []SkillData `json:"skills"`
CustomEnv map[string]string `json:"custom_env,omitempty"`
CustomArgs []string `json:"custom_args,omitempty"`
}
// SkillData represents a structured skill for task execution.

View File

@@ -24,7 +24,6 @@ type AgentResponse struct {
RuntimeMode string `json:"runtime_mode"`
RuntimeConfig any `json:"runtime_config"`
CustomEnv map[string]string `json:"custom_env"`
CustomArgs []string `json:"custom_args"`
CustomEnvRedacted bool `json:"custom_env_redacted"`
Visibility string `json:"visibility"`
Status string `json:"status"`
@@ -56,16 +55,6 @@ func agentToResponse(a db.Agent) AgentResponse {
customEnv = map[string]string{}
}
var customArgs []string
if a.CustomArgs != nil {
if err := json.Unmarshal(a.CustomArgs, &customArgs); err != nil {
slog.Warn("failed to unmarshal agent custom_args", "agent_id", uuidToString(a.ID), "error", err)
}
}
if customArgs == nil {
customArgs = []string{}
}
return AgentResponse{
ID: uuidToString(a.ID),
WorkspaceID: uuidToString(a.WorkspaceID),
@@ -77,7 +66,6 @@ func agentToResponse(a db.Agent) AgentResponse {
RuntimeMode: a.RuntimeMode,
RuntimeConfig: rc,
CustomEnv: customEnv,
CustomArgs: customArgs,
Visibility: a.Visibility,
Status: a.Status,
MaxConcurrentTasks: a.MaxConcurrentTasks,
@@ -129,7 +117,6 @@ type TaskAgentData struct {
Instructions string `json:"instructions"`
Skills []service.AgentSkillData `json:"skills,omitempty"`
CustomEnv map[string]string `json:"custom_env,omitempty"`
CustomArgs []string `json:"custom_args,omitempty"`
}
func taskToResponse(t db.AgentTaskQueue) AgentTaskResponse {
@@ -245,7 +232,6 @@ type CreateAgentRequest struct {
RuntimeID string `json:"runtime_id"`
RuntimeConfig any `json:"runtime_config"`
CustomEnv map[string]string `json:"custom_env"`
CustomArgs []string `json:"custom_args"`
Visibility string `json:"visibility"`
MaxConcurrentTasks int32 `json:"max_concurrent_tasks"`
}
@@ -298,11 +284,6 @@ func (h *Handler) CreateAgent(w http.ResponseWriter, r *http.Request) {
ce = []byte("{}")
}
ca, _ := json.Marshal(req.CustomArgs)
if req.CustomArgs == nil {
ca = []byte("[]")
}
agent, err := h.Queries.CreateAgent(r.Context(), db.CreateAgentParams{
WorkspaceID: parseUUID(workspaceID),
Name: req.Name,
@@ -316,7 +297,6 @@ func (h *Handler) CreateAgent(w http.ResponseWriter, r *http.Request) {
MaxConcurrentTasks: req.MaxConcurrentTasks,
OwnerID: parseUUID(ownerID),
CustomEnv: ce,
CustomArgs: ca,
})
if err != nil {
slog.Warn("create agent failed", append(logger.RequestAttrs(r), "error", err, "workspace_id", workspaceID)...)
@@ -346,7 +326,6 @@ type UpdateAgentRequest struct {
RuntimeID *string `json:"runtime_id"`
RuntimeConfig any `json:"runtime_config"`
CustomEnv *map[string]string `json:"custom_env"`
CustomArgs *[]string `json:"custom_args"`
Visibility *string `json:"visibility"`
Status *string `json:"status"`
MaxConcurrentTasks *int32 `json:"max_concurrent_tasks"`
@@ -431,10 +410,6 @@ func (h *Handler) UpdateAgent(w http.ResponseWriter, r *http.Request) {
ce, _ := json.Marshal(*req.CustomEnv)
params.CustomEnv = ce
}
if req.CustomArgs != nil {
ca, _ := json.Marshal(*req.CustomArgs)
params.CustomArgs = ca
}
if req.RuntimeID != nil {
runtime, err := h.Queries.GetAgentRuntimeForWorkspace(r.Context(), db.GetAgentRuntimeForWorkspaceParams{
ID: parseUUID(*req.RuntimeID),

View File

@@ -382,7 +382,7 @@ func (h *Handler) ClaimTaskByRuntime(w http.ResponseWriter, r *http.Request) {
return
}
// Build response with fresh agent data (name + skills + custom_env + custom_args).
// Build response with fresh agent data (name + skills + custom_env).
resp := taskToResponse(*task)
if agent, err := h.Queries.GetAgent(r.Context(), task.AgentID); err == nil {
skills := h.TaskService.LoadAgentSkills(r.Context(), task.AgentID)
@@ -392,19 +392,12 @@ func (h *Handler) ClaimTaskByRuntime(w http.ResponseWriter, r *http.Request) {
slog.Warn("failed to unmarshal agent custom_env", "agent_id", uuidToString(agent.ID), "error", err)
}
}
var customArgs []string
if agent.CustomArgs != nil {
if err := json.Unmarshal(agent.CustomArgs, &customArgs); err != nil {
slog.Warn("failed to unmarshal agent custom_args", "agent_id", uuidToString(agent.ID), "error", err)
}
}
resp.Agent = &TaskAgentData{
ID: uuidToString(agent.ID),
Name: agent.Name,
Instructions: agent.Instructions,
Skills: skills,
CustomEnv: customEnv,
CustomArgs: customArgs,
}
}

View File

@@ -1 +0,0 @@
ALTER TABLE agent DROP COLUMN custom_args;

View File

@@ -1,4 +0,0 @@
-- Add custom_args column to agent table for user-configurable CLI arguments
-- that get appended to the agent subprocess command at launch time.
-- Stored as JSONB array of strings (e.g. ["--model", "o3", "--max-turns", "50"]).
ALTER TABLE agent ADD COLUMN custom_args JSONB NOT NULL DEFAULT '[]';

View File

@@ -25,8 +25,7 @@ type ExecOptions struct {
SystemPrompt string
MaxTurns int
Timeout time.Duration
ResumeSessionID string // if non-empty, resume a previous agent session
CustomArgs []string // additional CLI arguments appended to the agent command
ResumeSessionID string // if non-empty, resume a previous agent session
}
// Session represents a running agent execution.

View File

@@ -34,10 +34,9 @@ func (b *claudeBackend) Execute(ctx context.Context, prompt string, opts ExecOpt
}
runCtx, cancel := context.WithTimeout(ctx, timeout)
args := buildClaudeArgs(opts, b.cfg.Logger)
args := buildClaudeArgs(opts)
cmd := exec.CommandContext(runCtx, execPath, args...)
b.cfg.Logger.Debug("agent command", "exec", execPath, "args", args)
cmd.WaitDelay = 10 * time.Second
if opts.Cwd != "" {
cmd.Dir = opts.Cwd
@@ -343,17 +342,7 @@ func trySend(ch chan<- Message, msg Message) {
}
}
// claudeBlockedArgs are flags hardcoded by the daemon that must not be
// overridden by user-configured custom_args. Overriding these would break
// the daemon↔Claude communication protocol.
var claudeBlockedArgs = map[string]blockedArgMode{
"-p": blockedStandalone, // non-interactive mode
"--output-format": blockedWithValue, // stream-json protocol
"--input-format": blockedWithValue, // stream-json protocol
"--permission-mode": blockedWithValue, // bypassPermissions for autonomous operation
}
func buildClaudeArgs(opts ExecOptions, logger *slog.Logger) []string {
func buildClaudeArgs(opts ExecOptions) []string {
args := []string{
"-p",
"--output-format", "stream-json",
@@ -374,7 +363,6 @@ func buildClaudeArgs(opts ExecOptions, logger *slog.Logger) []string {
if opts.ResumeSessionID != "" {
args = append(args, "--resume", opts.ResumeSessionID)
}
args = append(args, filterCustomArgs(opts.CustomArgs, claudeBlockedArgs, logger)...)
return args
}
@@ -434,52 +422,6 @@ func isFilteredChildEnvKey(key string) bool {
strings.HasPrefix(key, "CLAUDE_CODE_")
}
// blockedArgMode specifies whether a blocked arg takes a value or is standalone.
type blockedArgMode int
const (
blockedWithValue blockedArgMode = iota // flag takes a value (next arg or =value)
blockedStandalone // flag is boolean, no value
)
// filterCustomArgs removes protocol-critical flags from user-configured custom
// args to prevent breaking daemon↔agent communication. Each backend defines its
// own blocked set (the flags it hardcodes). This is intentionally narrow — we
// only block args that would break the communication protocol, not every
// possible dangerous flag. Workspace members are trusted to configure agents
// sensibly, same as with custom_env.
func filterCustomArgs(args []string, blocked map[string]blockedArgMode, logger *slog.Logger) []string {
if len(args) == 0 {
return args
}
filtered := make([]string, 0, len(args))
skip := false
for _, arg := range args {
if skip {
skip = false
continue
}
// Check if this arg is a blocked flag or starts with "blockedFlag=".
flag := arg
hasInlineValue := false
if idx := strings.Index(arg, "="); idx > 0 {
flag = arg[:idx]
hasInlineValue = true
}
mode, isBlocked := blocked[flag]
if isBlocked {
logger.Warn("custom_args: blocked protocol-critical flag, skipping", "flag", flag)
if mode == blockedWithValue && !hasInlineValue {
// The next arg is the value for this flag — skip it too.
skip = true
}
continue
}
filtered = append(filtered, arg)
}
return filtered
}
func detectCLIVersion(ctx context.Context, execPath string) (string, error) {
cmd := exec.CommandContext(ctx, execPath, "--version")
data, err := cmd.Output()

View File

@@ -197,7 +197,7 @@ func TestTrySendDropsWhenFull(t *testing.T) {
func TestBuildClaudeArgsIncludesStrictMCPConfig(t *testing.T) {
t.Parallel()
args := buildClaudeArgs(ExecOptions{}, slog.Default())
args := buildClaudeArgs(ExecOptions{})
expected := []string{
"-p",
"--output-format", "stream-json",
@@ -217,102 +217,6 @@ func TestBuildClaudeArgsIncludesStrictMCPConfig(t *testing.T) {
}
}
func TestFilterCustomArgsBlocksProtocolFlags(t *testing.T) {
t.Parallel()
blocked := map[string]blockedArgMode{
"--output-format": blockedWithValue,
"--permission-mode": blockedWithValue,
"-p": blockedStandalone,
}
logger := slog.Default()
// Blocks flag with separate value
result := filterCustomArgs([]string{"--output-format", "text", "--model", "o3"}, blocked, logger)
if len(result) != 2 || result[0] != "--model" || result[1] != "o3" {
t.Fatalf("expected [--model o3], got %v", result)
}
// Blocks flag=value form
result = filterCustomArgs([]string{"--permission-mode=plan", "--verbose"}, blocked, logger)
if len(result) != 1 || result[0] != "--verbose" {
t.Fatalf("expected [--verbose], got %v", result)
}
// Blocks standalone short flags without consuming next arg
result = filterCustomArgs([]string{"-p", "--max-turns", "10"}, blocked, logger)
if len(result) != 2 || result[0] != "--max-turns" || result[1] != "10" {
t.Fatalf("expected [--max-turns 10], got %v", result)
}
// Passes through non-blocked args
result = filterCustomArgs([]string{"--model", "o3", "--max-turns", "50"}, blocked, logger)
if len(result) != 4 {
t.Fatalf("expected all 4 args to pass through, got %v", result)
}
// Handles nil blocked map
result = filterCustomArgs([]string{"--anything"}, nil, logger)
if len(result) != 1 {
t.Fatalf("expected args to pass through with nil blocked map, got %v", result)
}
// Handles empty args
result = filterCustomArgs(nil, blocked, logger)
if result != nil {
t.Fatalf("expected nil for nil input, got %v", result)
}
}
func TestBuildClaudeArgsPassesThroughCustomArgs(t *testing.T) {
t.Parallel()
args := buildClaudeArgs(ExecOptions{
CustomArgs: []string{"--max-turns", "50", "--verbose"},
}, slog.Default())
// Custom args should appear at the end
found := 0
for i, a := range args {
if a == "--max-turns" && i+1 < len(args) && args[i+1] == "50" {
found++
}
}
if found != 1 {
t.Fatalf("expected --max-turns 50 in args: %v", args)
}
}
func TestBuildClaudeArgsFiltersBlockedCustomArgs(t *testing.T) {
t.Parallel()
args := buildClaudeArgs(ExecOptions{
CustomArgs: []string{"--output-format", "text", "--model", "o3"},
}, slog.Default())
// --output-format text should be stripped
for _, a := range args[len(args)-2:] {
if a == "text" {
// "text" should not be in the last args since --output-format was blocked
// The actual --output-format stream-json is earlier in the list
}
}
// --model o3 should pass through
foundModel := false
for i, a := range args {
if a == "--model" && i+1 < len(args) && args[i+1] == "o3" {
foundModel = true
}
// Verify no duplicate --output-format with value "text"
if a == "--output-format" && i+1 < len(args) && args[i+1] == "text" {
t.Fatalf("blocked --output-format text should have been filtered: %v", args)
}
}
if !foundModel {
t.Fatalf("expected --model o3 in args but it was missing: %v", args)
}
}
func TestBuildClaudeInputEncodesUserMessage(t *testing.T) {
t.Parallel()

View File

@@ -13,12 +13,6 @@ import (
"time"
)
// codexBlockedArgs are flags hardcoded by the daemon that must not be
// overridden by user-configured custom_args.
var codexBlockedArgs = map[string]blockedArgMode{
"--listen": blockedWithValue, // stdio:// transport for daemon communication
}
// codexBackend implements Backend by spawning `codex app-server --listen stdio://`
// and communicating via JSON-RPC 2.0 over stdin/stdout.
type codexBackend struct {
@@ -40,9 +34,7 @@ func (b *codexBackend) Execute(ctx context.Context, prompt string, opts ExecOpti
}
runCtx, cancel := context.WithTimeout(ctx, timeout)
codexArgs := append([]string{"app-server", "--listen", "stdio://"}, filterCustomArgs(opts.CustomArgs, codexBlockedArgs, b.cfg.Logger)...)
cmd := exec.CommandContext(runCtx, execPath, codexArgs...)
b.cfg.Logger.Debug("agent command", "exec", execPath, "args", codexArgs)
cmd := exec.CommandContext(runCtx, execPath, "app-server", "--listen", "stdio://")
if opts.Cwd != "" {
cmd.Dir = opts.Cwd
}

View File

@@ -5,7 +5,6 @@ import (
"context"
"encoding/json"
"fmt"
"log/slog"
"os/exec"
"strings"
"time"
@@ -32,10 +31,9 @@ func (b *geminiBackend) Execute(ctx context.Context, prompt string, opts ExecOpt
}
runCtx, cancel := context.WithTimeout(ctx, timeout)
args := buildGeminiArgs(prompt, opts, b.cfg.Logger)
args := buildGeminiArgs(prompt, opts)
cmd := exec.CommandContext(runCtx, execPath, args...)
b.cfg.Logger.Debug("agent command", "exec", execPath, "args", args)
cmd.WaitDelay = 10 * time.Second
if opts.Cwd != "" {
cmd.Dir = opts.Cwd
@@ -241,15 +239,7 @@ type geminiModelStats struct {
// -o stream-json streaming NDJSON output for live events
// -m <model> optional model override
// -r <session> resume a previous session (if provided)
// geminiBlockedArgs are flags hardcoded by the daemon that must not be
// overridden by user-configured custom_args.
var geminiBlockedArgs = map[string]blockedArgMode{
"-p": blockedWithValue, // non-interactive prompt
"--yolo": blockedStandalone, // auto-approve tool use
"-o": blockedWithValue, // stream-json output format
}
func buildGeminiArgs(prompt string, opts ExecOptions, logger *slog.Logger) []string {
func buildGeminiArgs(prompt string, opts ExecOptions) []string {
args := []string{
"-p", prompt,
"--yolo",
@@ -261,6 +251,5 @@ func buildGeminiArgs(prompt string, opts ExecOptions, logger *slog.Logger) []str
if opts.ResumeSessionID != "" {
args = append(args, "-r", opts.ResumeSessionID)
}
args = append(args, filterCustomArgs(opts.CustomArgs, geminiBlockedArgs, logger)...)
return args
}

View File

@@ -1,14 +1,13 @@
package agent
import (
"log/slog"
"testing"
)
func TestBuildGeminiArgsBaseline(t *testing.T) {
t.Parallel()
args := buildGeminiArgs("write a haiku", ExecOptions{}, slog.Default())
args := buildGeminiArgs("write a haiku", ExecOptions{})
expected := []string{
"-p", "write a haiku",
"--yolo",
@@ -28,7 +27,7 @@ func TestBuildGeminiArgsBaseline(t *testing.T) {
func TestBuildGeminiArgsWithModel(t *testing.T) {
t.Parallel()
args := buildGeminiArgs("hi", ExecOptions{Model: "gemini-2.5-pro"}, slog.Default())
args := buildGeminiArgs("hi", ExecOptions{Model: "gemini-2.5-pro"})
var foundModel bool
for i, a := range args {
@@ -48,7 +47,7 @@ func TestBuildGeminiArgsWithModel(t *testing.T) {
func TestBuildGeminiArgsWithResume(t *testing.T) {
t.Parallel()
args := buildGeminiArgs("hi", ExecOptions{ResumeSessionID: "3"}, slog.Default())
args := buildGeminiArgs("hi", ExecOptions{ResumeSessionID: "3"})
var foundResume bool
for i, a := range args {
@@ -68,7 +67,7 @@ func TestBuildGeminiArgsWithResume(t *testing.T) {
func TestBuildGeminiArgsOmitsModelWhenEmpty(t *testing.T) {
t.Parallel()
args := buildGeminiArgs("hi", ExecOptions{}, slog.Default())
args := buildGeminiArgs("hi", ExecOptions{})
for _, a := range args {
if a == "-m" {
t.Fatalf("expected no -m flag when Model is empty, got args=%v", args)
@@ -78,33 +77,3 @@ func TestBuildGeminiArgsOmitsModelWhenEmpty(t *testing.T) {
}
}
}
func TestBuildGeminiArgsPassesThroughCustomArgs(t *testing.T) {
t.Parallel()
args := buildGeminiArgs("hi", ExecOptions{
CustomArgs: []string{"--sandbox"},
}, slog.Default())
if args[len(args)-1] != "--sandbox" {
t.Fatalf("expected --sandbox at end of args, got %v", args)
}
}
func TestBuildGeminiArgsFiltersBlockedCustomArgs(t *testing.T) {
t.Parallel()
args := buildGeminiArgs("hi", ExecOptions{
CustomArgs: []string{"-o", "text", "--sandbox"},
}, slog.Default())
// -o text should be filtered, --sandbox should pass through
for i, a := range args {
if a == "-o" && i+1 < len(args) && args[i+1] == "text" {
t.Fatalf("blocked -o text should have been filtered: %v", args)
}
}
if args[len(args)-1] != "--sandbox" {
t.Fatalf("expected --sandbox to pass through, got %v", args)
}
}

View File

@@ -34,9 +34,7 @@ func (b *hermesBackend) Execute(ctx context.Context, prompt string, opts ExecOpt
}
runCtx, cancel := context.WithTimeout(ctx, timeout)
hermesArgs := append([]string{"acp"}, opts.CustomArgs...)
cmd := exec.CommandContext(runCtx, execPath, hermesArgs...)
b.cfg.Logger.Debug("agent command", "exec", execPath, "args", hermesArgs)
cmd := exec.CommandContext(runCtx, execPath, "acp")
if opts.Cwd != "" {
cmd.Dir = opts.Cwd
}

View File

@@ -11,15 +11,6 @@ import (
"time"
)
// openclawBlockedArgs are flags hardcoded by the daemon that must not be
// overridden by user-configured custom_args.
var openclawBlockedArgs = map[string]blockedArgMode{
"--local": blockedStandalone, // local mode for daemon execution
"--json": blockedStandalone, // JSON output for daemon communication
"--session-id": blockedWithValue, // managed by daemon for session resumption
"--message": blockedWithValue, // prompt is set by daemon
}
// openclawBackend implements Backend by spawning `openclaw agent --message <prompt>
// --output-format stream-json --yes` and reading streaming NDJSON events from
// stdout — similar to the opencode backend.
@@ -56,11 +47,9 @@ func (b *openclawBackend) Execute(ctx context.Context, prompt string, opts ExecO
if opts.Timeout > 0 {
args = append(args, "--timeout", fmt.Sprintf("%d", int(opts.Timeout.Seconds())))
}
args = append(args, filterCustomArgs(opts.CustomArgs, openclawBlockedArgs, b.cfg.Logger)...)
args = append(args, "--message", prompt)
cmd := exec.CommandContext(runCtx, execPath, args...)
b.cfg.Logger.Debug("agent command", "exec", execPath, "args", args)
cmd.WaitDelay = 10 * time.Second
if opts.Cwd != "" {
cmd.Dir = opts.Cwd

View File

@@ -11,12 +11,6 @@ import (
"time"
)
// opencodeBlockedArgs are flags hardcoded by the daemon that must not be
// overridden by user-configured custom_args.
var opencodeBlockedArgs = map[string]blockedArgMode{
"--format": blockedWithValue, // json output format for daemon communication
}
// opencodeBackend implements Backend by spawning `opencode run --format json`
// and reading streaming JSON events from stdout — the same pattern as Claude.
type opencodeBackend struct {
@@ -51,11 +45,9 @@ func (b *opencodeBackend) Execute(ctx context.Context, prompt string, opts ExecO
if opts.ResumeSessionID != "" {
args = append(args, "--session", opts.ResumeSessionID)
}
args = append(args, filterCustomArgs(opts.CustomArgs, opencodeBlockedArgs, b.cfg.Logger)...)
args = append(args, prompt)
cmd := exec.CommandContext(runCtx, execPath, args...)
b.cfg.Logger.Debug("agent command", "exec", execPath, "args", args)
cmd.WaitDelay = 10 * time.Second
if opts.Cwd != "" {
cmd.Dir = opts.Cwd

View File

@@ -14,7 +14,7 @@ import (
const archiveAgent = `-- name: ArchiveAgent :one
UPDATE agent SET archived_at = now(), archived_by = $2, updated_at = now()
WHERE id = $1
RETURNING id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args
RETURNING id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env
`
type ArchiveAgentParams struct {
@@ -44,7 +44,6 @@ func (q *Queries) ArchiveAgent(ctx context.Context, arg ArchiveAgentParams) (Age
&i.ArchivedAt,
&i.ArchivedBy,
&i.CustomEnv,
&i.CustomArgs,
)
return i, err
}
@@ -218,9 +217,9 @@ const createAgent = `-- name: CreateAgent :one
INSERT INTO agent (
workspace_id, name, description, avatar_url, runtime_mode,
runtime_config, runtime_id, visibility, max_concurrent_tasks, owner_id,
instructions, custom_env, custom_args
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
RETURNING id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args
instructions, custom_env
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
RETURNING id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env
`
type CreateAgentParams struct {
@@ -236,7 +235,6 @@ type CreateAgentParams struct {
OwnerID pgtype.UUID `json:"owner_id"`
Instructions string `json:"instructions"`
CustomEnv []byte `json:"custom_env"`
CustomArgs []byte `json:"custom_args"`
}
func (q *Queries) CreateAgent(ctx context.Context, arg CreateAgentParams) (Agent, error) {
@@ -253,7 +251,6 @@ func (q *Queries) CreateAgent(ctx context.Context, arg CreateAgentParams) (Agent
arg.OwnerID,
arg.Instructions,
arg.CustomEnv,
arg.CustomArgs,
)
var i Agent
err := row.Scan(
@@ -275,7 +272,6 @@ func (q *Queries) CreateAgent(ctx context.Context, arg CreateAgentParams) (Agent
&i.ArchivedAt,
&i.ArchivedBy,
&i.CustomEnv,
&i.CustomArgs,
)
return i, err
}
@@ -407,7 +403,7 @@ func (q *Queries) FailStaleTasks(ctx context.Context, arg FailStaleTasksParams)
}
const getAgent = `-- name: GetAgent :one
SELECT id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args FROM agent
SELECT id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env FROM agent
WHERE id = $1
`
@@ -433,13 +429,12 @@ func (q *Queries) GetAgent(ctx context.Context, id pgtype.UUID) (Agent, error) {
&i.ArchivedAt,
&i.ArchivedBy,
&i.CustomEnv,
&i.CustomArgs,
)
return i, err
}
const getAgentInWorkspace = `-- name: GetAgentInWorkspace :one
SELECT id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args FROM agent
SELECT id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env FROM agent
WHERE id = $1 AND workspace_id = $2
`
@@ -470,7 +465,6 @@ func (q *Queries) GetAgentInWorkspace(ctx context.Context, arg GetAgentInWorkspa
&i.ArchivedAt,
&i.ArchivedBy,
&i.CustomEnv,
&i.CustomArgs,
)
return i, err
}
@@ -671,7 +665,7 @@ func (q *Queries) ListAgentTasks(ctx context.Context, agentID pgtype.UUID) ([]Ag
}
const listAgents = `-- name: ListAgents :many
SELECT id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args FROM agent
SELECT id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env FROM agent
WHERE workspace_id = $1 AND archived_at IS NULL
ORDER BY created_at ASC
`
@@ -704,7 +698,6 @@ func (q *Queries) ListAgents(ctx context.Context, workspaceID pgtype.UUID) ([]Ag
&i.ArchivedAt,
&i.ArchivedBy,
&i.CustomEnv,
&i.CustomArgs,
); err != nil {
return nil, err
}
@@ -717,7 +710,7 @@ func (q *Queries) ListAgents(ctx context.Context, workspaceID pgtype.UUID) ([]Ag
}
const listAllAgents = `-- name: ListAllAgents :many
SELECT id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args FROM agent
SELECT id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env FROM agent
WHERE workspace_id = $1
ORDER BY created_at ASC
`
@@ -750,7 +743,6 @@ func (q *Queries) ListAllAgents(ctx context.Context, workspaceID pgtype.UUID) ([
&i.ArchivedAt,
&i.ArchivedBy,
&i.CustomEnv,
&i.CustomArgs,
); err != nil {
return nil, err
}
@@ -855,7 +847,7 @@ func (q *Queries) ListTasksByIssue(ctx context.Context, issueID pgtype.UUID) ([]
const restoreAgent = `-- name: RestoreAgent :one
UPDATE agent SET archived_at = NULL, archived_by = NULL, updated_at = now()
WHERE id = $1
RETURNING id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args
RETURNING id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env
`
func (q *Queries) RestoreAgent(ctx context.Context, id pgtype.UUID) (Agent, error) {
@@ -880,7 +872,6 @@ func (q *Queries) RestoreAgent(ctx context.Context, id pgtype.UUID) (Agent, erro
&i.ArchivedAt,
&i.ArchivedBy,
&i.CustomEnv,
&i.CustomArgs,
)
return i, err
}
@@ -931,10 +922,9 @@ UPDATE agent SET
max_concurrent_tasks = COALESCE($10, max_concurrent_tasks),
instructions = COALESCE($11, instructions),
custom_env = COALESCE($12, custom_env),
custom_args = COALESCE($13, custom_args),
updated_at = now()
WHERE id = $1
RETURNING id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args
RETURNING id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env
`
type UpdateAgentParams struct {
@@ -950,7 +940,6 @@ type UpdateAgentParams struct {
MaxConcurrentTasks pgtype.Int4 `json:"max_concurrent_tasks"`
Instructions pgtype.Text `json:"instructions"`
CustomEnv []byte `json:"custom_env"`
CustomArgs []byte `json:"custom_args"`
}
func (q *Queries) UpdateAgent(ctx context.Context, arg UpdateAgentParams) (Agent, error) {
@@ -967,7 +956,6 @@ func (q *Queries) UpdateAgent(ctx context.Context, arg UpdateAgentParams) (Agent
arg.MaxConcurrentTasks,
arg.Instructions,
arg.CustomEnv,
arg.CustomArgs,
)
var i Agent
err := row.Scan(
@@ -989,7 +977,6 @@ func (q *Queries) UpdateAgent(ctx context.Context, arg UpdateAgentParams) (Agent
&i.ArchivedAt,
&i.ArchivedBy,
&i.CustomEnv,
&i.CustomArgs,
)
return i, err
}
@@ -997,7 +984,7 @@ func (q *Queries) UpdateAgent(ctx context.Context, arg UpdateAgentParams) (Agent
const updateAgentStatus = `-- name: UpdateAgentStatus :one
UPDATE agent SET status = $2, updated_at = now()
WHERE id = $1
RETURNING id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args
RETURNING id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env
`
type UpdateAgentStatusParams struct {
@@ -1027,7 +1014,6 @@ func (q *Queries) UpdateAgentStatus(ctx context.Context, arg UpdateAgentStatusPa
&i.ArchivedAt,
&i.ArchivedBy,
&i.CustomEnv,
&i.CustomArgs,
)
return i, err
}

View File

@@ -38,7 +38,6 @@ type Agent struct {
ArchivedAt pgtype.Timestamptz `json:"archived_at"`
ArchivedBy pgtype.UUID `json:"archived_by"`
CustomEnv []byte `json:"custom_env"`
CustomArgs []byte `json:"custom_args"`
}
type AgentRuntime struct {

View File

@@ -20,8 +20,8 @@ WHERE id = $1 AND workspace_id = $2;
INSERT INTO agent (
workspace_id, name, description, avatar_url, runtime_mode,
runtime_config, runtime_id, visibility, max_concurrent_tasks, owner_id,
instructions, custom_env, custom_args
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
instructions, custom_env
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
RETURNING *;
-- name: UpdateAgent :one
@@ -37,7 +37,6 @@ UPDATE agent SET
max_concurrent_tasks = COALESCE(sqlc.narg('max_concurrent_tasks'), max_concurrent_tasks),
instructions = COALESCE(sqlc.narg('instructions'), instructions),
custom_env = COALESCE(sqlc.narg('custom_env'), custom_env),
custom_args = COALESCE(sqlc.narg('custom_args'), custom_args),
updated_at = now()
WHERE id = $1
RETURNING *;