0ec3db81e6
CR2 review findings on PR #13 (branch feat/management-cli-verbs): 1. [HIGH] PathEscape user-controlled path segments. platform.go built paths via fmt.Sprintf on raw caller IDs (GetWorkspace/DeleteWorkspace/ RestartWorkspace/ListWorkspaceAgents/GetAgent/GetPeers/GetDelegations) and the agent-send / workspace-delegate runHTTP call sites concatenated raw IDs. An ID with '/', '?' or '#' could alter the endpoint or leak into the query. Wrapped every caller-supplied segment in url.PathEscape (management.go already did this). DeleteWorkspace's ?confirm=true is now injection-safe. Severity note: this runs under the user's own management creds, so it is primarily robustness/correctness rather than a privilege-escalation hole. 2. [MED] Config not bound to globals. viper read the config file but the flag-backed apiURL/outputFormat globals were never populated from it, so `molecule config set api_url` did not affect newClient()/cpURL(). Added applyConfigDefaults(): config file is adopted only when no env override and the global is still at its built-in default, so precedence stays flag > env > config file > default. 3. [MED] MintWorkspaceToken sent a nil body → JSON `null`. Now sends an empty object (struct{}{}) → `{}`, matching sibling tooling and avoiding rejection by a handler that decodes into a struct/map. 4. [MED] cpURL defaulted to apiURL (tenant host), so an unset MOLECULE_CP_URL would send the privileged CP-admin bearer to a tenant host. cpURL() no longer falls back to apiURL; cpAdminClient() now requires an explicit MOLECULE_CP_URL and fails fast otherwise. Updated org.go help text. 5. [LOW] config set now os.MkdirAll's the config dir before WriteConfig/ SafeWriteConfig, which otherwise fail on a fresh machine where ~/.config doesn't exist yet. Tests: added path-segment escaping coverage (platform + delete), MintWorkspaceToken body={}, applyConfigDefaults precedence, config-set mkdir, and CP-admin credential targeting; retargeted TestCPURLFallback → TestCPURLNoTenantFallback. go build/vet/test all green; gofmt clean on edited files. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
186 lines
4.9 KiB
Go
186 lines
4.9 KiB
Go
// Package cmd implements the CLI command tree.
|
|
package cmd
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"text/tabwriter"
|
|
|
|
"github.com/spf13/cobra"
|
|
"go.moleculesai.app/cli/internal/client"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Agent command group
|
|
// ---------------------------------------------------------------------------
|
|
|
|
var agentCmd = &cobra.Command{
|
|
Use: "agent",
|
|
Short: "Inspect and interact with agents",
|
|
Long: `List agents, inspect individual agents, send messages, and discover peers.`,
|
|
}
|
|
|
|
func init() {
|
|
agentCmd.AddCommand(
|
|
agentListCmd, agentInspectCmd, agentSendCmd, agentPeersCmd,
|
|
)
|
|
}
|
|
|
|
// ===========================================================================
|
|
// mol agent list
|
|
// ===========================================================================
|
|
var agentListCmd = &cobra.Command{
|
|
Use: "list [workspace-id]",
|
|
Short: "List all agents (optionally filtered to one workspace)",
|
|
Args: cobra.RangeArgs(0, 1),
|
|
RunE: runAgentList,
|
|
}
|
|
|
|
func runAgentList(cmd *cobra.Command, args []string) error {
|
|
cl := newClient()
|
|
var agents []client.Agent
|
|
var err error
|
|
if len(args) == 0 {
|
|
agents, err = cl.ListAgents()
|
|
} else {
|
|
agents, err = cl.ListWorkspaceAgents(args[0])
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("agent list: %w", err)
|
|
}
|
|
if outputFormat == "json" {
|
|
return printJSON(agents)
|
|
}
|
|
if outputFormat == "yaml" {
|
|
return printYAML(agents)
|
|
}
|
|
if len(agents) == 0 {
|
|
fmt.Println("No agents found.")
|
|
return nil
|
|
}
|
|
w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
|
fmt.Fprintln(w, "ID\tNAME\tWORKSPACE\tSTATUS\tMODEL\tRUNTIME")
|
|
for _, a := range agents {
|
|
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
|
|
a.ID, a.Name, a.WorkspaceID, a.Status, a.Model, a.Runtime)
|
|
}
|
|
return w.Flush()
|
|
}
|
|
|
|
// ===========================================================================
|
|
// mol agent inspect
|
|
// ===========================================================================
|
|
var agentInspectCmd = &cobra.Command{
|
|
Use: "inspect <agent-id>",
|
|
Short: "Show full details for an agent",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: runAgentInspect,
|
|
}
|
|
|
|
func runAgentInspect(cmd *cobra.Command, args []string) error {
|
|
cl := newClient()
|
|
a, err := cl.GetAgent(args[0])
|
|
if err != nil {
|
|
return fmt.Errorf("agent inspect: %w", err)
|
|
}
|
|
if outputFormat == "json" {
|
|
return printJSON(a)
|
|
}
|
|
if outputFormat == "yaml" {
|
|
return printYAML(a)
|
|
}
|
|
w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
|
kv(w, "ID", a.ID)
|
|
kv(w, "Name", a.Name)
|
|
kv(w, "WorkspaceID", a.WorkspaceID)
|
|
kv(w, "Status", a.Status)
|
|
kv(w, "Model", a.Model)
|
|
kv(w, "Runtime", a.Runtime)
|
|
kv(w, "CreatedAt", a.CreatedAt)
|
|
return w.Flush()
|
|
}
|
|
|
|
// ===========================================================================
|
|
// mol agent send
|
|
// ===========================================================================
|
|
var agentSendCmd = &cobra.Command{
|
|
Use: "send <agent-id> <message>",
|
|
Short: "Send a one-shot message to an agent via A2A",
|
|
Args: cobra.ExactArgs(2),
|
|
RunE: runAgentSend,
|
|
}
|
|
|
|
func runAgentSend(cmd *cobra.Command, args []string) error {
|
|
agentID, message := args[0], args[1]
|
|
cl := newClient()
|
|
|
|
a, err := cl.GetAgent(agentID)
|
|
if err != nil {
|
|
return fmt.Errorf("agent send: %w", err)
|
|
}
|
|
wsID := a.WorkspaceID
|
|
if wsID == "" {
|
|
return fmt.Errorf("agent send: workspace_id unknown for agent %q", agentID)
|
|
}
|
|
|
|
type a2aReq struct {
|
|
AgentID string `json:"agent_id"`
|
|
Message string `json:"message"`
|
|
}
|
|
type a2aResp struct {
|
|
Result string `json:"result,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
encoded, _ := json.Marshal(a2aReq{AgentID: agentID, Message: message})
|
|
body, err := runHTTP("POST", cl.BaseURL+"/workspaces/"+url.PathEscape(wsID)+"/a2a", encoded)
|
|
if err != nil {
|
|
return fmt.Errorf("agent send: %w", err)
|
|
}
|
|
var resp a2aResp
|
|
if err := json.Unmarshal(body, &resp); err != nil {
|
|
return fmt.Errorf("agent send: parse response: %w", err)
|
|
}
|
|
if resp.Error != "" {
|
|
return fmt.Errorf("agent send: platform error: %s", resp.Error)
|
|
}
|
|
fmt.Println(resp.Result)
|
|
return nil
|
|
}
|
|
|
|
// ===========================================================================
|
|
// mol agent peers
|
|
// ===========================================================================
|
|
var agentPeersCmd = &cobra.Command{
|
|
Use: "peers <workspace-id>",
|
|
Short: "List peer workspaces reachable from a workspace",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: runAgentPeers,
|
|
}
|
|
|
|
func runAgentPeers(cmd *cobra.Command, args []string) error {
|
|
cl := newClient()
|
|
peers, err := cl.GetPeers(args[0])
|
|
if err != nil {
|
|
return fmt.Errorf("agent peers: %w", err)
|
|
}
|
|
if outputFormat == "json" {
|
|
return printJSON(peers)
|
|
}
|
|
if outputFormat == "yaml" {
|
|
return printYAML(peers)
|
|
}
|
|
if len(peers) == 0 {
|
|
fmt.Println("No peers found.")
|
|
return nil
|
|
}
|
|
w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
|
fmt.Fprintln(w, "ID\tNAME\tWORKSPACE\tSTATUS\tMODEL")
|
|
for _, p := range peers {
|
|
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
|
|
p.ID, p.Name, p.WorkspaceID, p.Status, p.Model)
|
|
}
|
|
return w.Flush()
|
|
}
|