mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-18 04:09:13 +02:00
Compare commits
1 Commits
feat/agent
...
fix/self-h
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be8a2040f5 |
12
.env.example
12
.env.example
@@ -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
15
Caddyfile
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE agent DROP COLUMN custom_args;
|
||||
@@ -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 '[]';
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 *;
|
||||
|
||||
Reference in New Issue
Block a user