Files
osmedeus/internal/functions/util_functions.go
T
j3ssie 520977d1ea feat: Add agent-acp step type with ACP protocol support
- Implement agent-acp step type for spawning external ACP agent subprocesses via Agent Communication Protocol
- Add ACPExecutor with validation, field rendering, and subprocess lifecycle management
- Integrate agent-acp field rendering in StepDispatcher (batch and sequential modes)
- Add run_agent() utility function for workflows to execute ACP agents from steps and JS context
- Add osmedeus agent CLI command for interactive agent execution with --agent, --cwd, --timeout, --stdin, and --list flags
- Add /osm/api/agent/chat/completions REST endpoint with OpenAI-compatible chat format and concurrency control
- Support agent selection via: built-in names (claude-code, codex, opencode, gemini) or custom acp_config.command
- Add step-level configuration: cwd, allowed_paths, acp_config (command, args, env, write_enabled)
- Add comprehensive E2E tests for agent-acp workflows (basic, minimal, config, codex variants)
- Add test workflows in test/testdata/workflows/agent-and-llm/
- Update AGENTS.md documentation with agent-acp examples, CLI usage, and API endpoints
2026-02-27 16:18:06 +08:00

1635 lines
53 KiB
Go

package functions
import (
"bytes"
"context"
"crypto/tls"
"encoding/base64"
"fmt"
"io"
mrand "math/rand"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/dop251/goja"
"github.com/google/uuid"
"github.com/j3ssie/osmedeus/v5/internal/core"
"github.com/j3ssie/osmedeus/v5/internal/logger"
"github.com/j3ssie/osmedeus/v5/internal/retry"
"github.com/j3ssie/osmedeus/v5/internal/terminal"
"go.uber.org/zap"
)
// httpRequest makes an HTTP request with custom method, headers, and body
// Usage: httpRequest(url, method, headers, body)
// headers is an object like {"Content-Type": "application/json", "Authorization": "Bearer token"}
// Returns: {statusCode: int, body: string, headers: object, error: string}
func (vf *vmFunc) httpRequest(call goja.FunctionCall) goja.Value {
urlStr := call.Argument(0).String()
method := call.Argument(1).String()
logger.Get().Debug("Calling "+terminal.HiGreen("httpRequest"), zap.String("url", urlStr), zap.String("method", method))
if urlStr == "undefined" || urlStr == "" {
logger.Get().Warn("httpRequest: url is required")
return vf.vm.ToValue(map[string]interface{}{
"statusCode": 0,
"body": "",
"headers": map[string]string{},
"error": "url is required",
})
}
if method == "undefined" || method == "" {
method = "GET"
}
// Parse headers
var headers map[string]interface{}
if !goja.IsUndefined(call.Argument(2)) {
exported := call.Argument(2).Export()
if h, ok := exported.(map[string]interface{}); ok {
headers = h
}
}
// Parse body
var body io.Reader
bodyStr := call.Argument(3).String()
if bodyStr != "undefined" && bodyStr != "" {
body = bytes.NewBufferString(bodyStr)
}
// Create request
req, err := http.NewRequest(method, urlStr, body)
if err != nil {
logger.Get().Warn("httpRequest: failed to create request", zap.String("url", urlStr), zap.Error(err))
return vf.vm.ToValue(map[string]interface{}{
"statusCode": 0,
"body": "",
"headers": map[string]string{},
"error": err.Error(),
})
}
// Set default User-Agent if not provided
if headers == nil {
headers = make(map[string]interface{})
}
if _, hasUA := headers["User-Agent"]; !hasUA {
headers["User-Agent"] = core.DefaultUA
}
// Set headers
for k, v := range headers {
if str, ok := v.(string); ok {
req.Header.Set(k, str)
}
}
// Create client with timeout and TLS skip verify
client := &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
// Execute request with retry logic (3 attempts, exponential backoff)
var resp *http.Response
ctx := context.Background()
retryCfg := retry.Config{
MaxAttempts: 3,
InitialDelay: 200 * time.Millisecond,
MaxDelay: 2 * time.Second,
Multiplier: 2.0,
}
err = retry.Do(ctx, retryCfg, func() error {
var reqErr error
// Need to recreate body for retries since it may have been consumed
if bodyStr != "undefined" && bodyStr != "" {
req.Body = io.NopCloser(bytes.NewBufferString(bodyStr))
}
resp, reqErr = client.Do(req)
if reqErr != nil {
return retry.Retryable(reqErr)
}
// Retry on server errors (5xx)
if resp.StatusCode >= 500 {
_ = resp.Body.Close()
return retry.Retryable(fmt.Errorf("server error: %d", resp.StatusCode))
}
return nil
})
if err != nil {
logger.Get().Warn("httpRequest: request failed after retries", zap.String("url", urlStr), zap.Error(err))
return vf.vm.ToValue(map[string]interface{}{
"statusCode": 0,
"body": "",
"headers": map[string]string{},
"error": err.Error(),
})
}
defer func() { _ = resp.Body.Close() }()
// Read response body
respBody, err := io.ReadAll(resp.Body)
if err != nil {
logger.Get().Warn("httpRequest: failed to read response body", zap.String("url", urlStr), zap.Error(err))
return vf.vm.ToValue(map[string]interface{}{
"statusCode": resp.StatusCode,
"body": "",
"headers": map[string]string{},
"error": err.Error(),
})
}
// Extract response headers
respHeaders := make(map[string]string)
for k, v := range resp.Header {
if len(v) > 0 {
respHeaders[k] = v[0]
}
}
logger.Get().Debug(terminal.HiGreen("httpRequest")+" result", zap.String("url", urlStr), zap.Int("statusCode", resp.StatusCode), zap.Int("bodySize", len(respBody)))
return vf.vm.ToValue(map[string]interface{}{
"statusCode": resp.StatusCode,
"body": string(respBody),
"headers": respHeaders,
"error": "",
})
}
// httpGet makes a GET request to the specified URL
// Usage: http_get(url)
// Returns: {status_code, error, message, response_headers, response_body, content_length, response_time_ms}
func (vf *vmFunc) httpGet(call goja.FunctionCall) goja.Value {
start := time.Now()
urlStr := call.Argument(0).String()
logger.Get().Debug("Calling "+terminal.HiGreen("http_get"), zap.String("url", urlStr))
if urlStr == "undefined" || urlStr == "" {
logger.Get().Warn("http_get: url is required")
return vf.vm.ToValue(map[string]interface{}{
"status_code": 0,
"error": "url is required",
"message": "error",
"response_headers": map[string]string{},
"response_body": "",
"content_length": 0,
"response_time_ms": time.Since(start).Milliseconds(),
})
}
// Create request
req, err := http.NewRequest("GET", urlStr, nil)
if err != nil {
logger.Get().Warn("http_get: failed to create request", zap.String("url", urlStr), zap.Error(err))
return vf.vm.ToValue(map[string]interface{}{
"status_code": 0,
"error": err.Error(),
"message": "error",
"response_headers": map[string]string{},
"response_body": "",
"content_length": 0,
"response_time_ms": time.Since(start).Milliseconds(),
})
}
// Set default User-Agent
req.Header.Set("User-Agent", core.DefaultUA)
// Create client with timeout and TLS skip verify
client := &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
// Execute request with retry logic (3 attempts, exponential backoff)
var resp *http.Response
ctx := context.Background()
retryCfg := retry.Config{
MaxAttempts: 3,
InitialDelay: 200 * time.Millisecond,
MaxDelay: 2 * time.Second,
Multiplier: 2.0,
}
err = retry.Do(ctx, retryCfg, func() error {
var reqErr error
resp, reqErr = client.Do(req)
if reqErr != nil {
return retry.Retryable(reqErr)
}
// Retry on server errors (5xx)
if resp.StatusCode >= 500 {
_ = resp.Body.Close()
return retry.Retryable(fmt.Errorf("server error: %d", resp.StatusCode))
}
return nil
})
if err != nil {
logger.Get().Warn("http_get: request failed after retries", zap.String("url", urlStr), zap.Error(err))
return vf.vm.ToValue(map[string]interface{}{
"status_code": 0,
"error": err.Error(),
"message": "error",
"response_headers": map[string]string{},
"response_body": "",
"content_length": 0,
"response_time_ms": time.Since(start).Milliseconds(),
})
}
defer func() { _ = resp.Body.Close() }()
// Read response body
respBody, err := io.ReadAll(resp.Body)
if err != nil {
logger.Get().Warn("http_get: failed to read response body", zap.String("url", urlStr), zap.Error(err))
return vf.vm.ToValue(map[string]interface{}{
"status_code": resp.StatusCode,
"error": err.Error(),
"message": "error",
"response_headers": map[string]string{},
"response_body": "",
"content_length": 0,
"response_time_ms": time.Since(start).Milliseconds(),
})
}
// Extract response headers
respHeaders := make(map[string]string)
for k, v := range resp.Header {
if len(v) > 0 {
respHeaders[k] = v[0]
}
}
elapsed := time.Since(start).Milliseconds()
logger.Get().Debug(terminal.HiGreen("http_get")+" result", zap.String("url", urlStr), zap.Int("status_code", resp.StatusCode), zap.Int("bodySize", len(respBody)), zap.Int64("response_time_ms", elapsed))
return vf.vm.ToValue(map[string]interface{}{
"status_code": resp.StatusCode,
"error": nil,
"message": "success",
"response_headers": respHeaders,
"response_body": string(respBody),
"content_length": len(respBody),
"response_time_ms": elapsed,
})
}
// httpPost makes a POST request to the specified URL with the given body
// Usage: http_post(url, body)
// Returns: {status_code, error, message, response_headers, response_body, content_length, response_time_ms}
func (vf *vmFunc) httpPost(call goja.FunctionCall) goja.Value {
start := time.Now()
urlStr := call.Argument(0).String()
bodyStr := call.Argument(1).String()
logger.Get().Debug("Calling "+terminal.HiGreen("http_post"), zap.String("url", urlStr), zap.Int("bodyLength", len(bodyStr)))
if urlStr == "undefined" || urlStr == "" {
logger.Get().Warn("http_post: url is required")
return vf.vm.ToValue(map[string]interface{}{
"status_code": 0,
"error": "url is required",
"message": "error",
"response_headers": map[string]string{},
"response_body": "",
"content_length": 0,
"response_time_ms": time.Since(start).Milliseconds(),
})
}
// Handle undefined body as empty string
var body io.Reader
if bodyStr != "undefined" && bodyStr != "" {
body = bytes.NewBufferString(bodyStr)
}
// Create request
req, err := http.NewRequest("POST", urlStr, body)
if err != nil {
logger.Get().Warn("http_post: failed to create request", zap.String("url", urlStr), zap.Error(err))
return vf.vm.ToValue(map[string]interface{}{
"status_code": 0,
"error": err.Error(),
"message": "error",
"response_headers": map[string]string{},
"response_body": "",
"content_length": 0,
"response_time_ms": time.Since(start).Milliseconds(),
})
}
// Set default User-Agent and Content-Type
req.Header.Set("User-Agent", core.DefaultUA)
if bodyStr != "undefined" && bodyStr != "" {
req.Header.Set("Content-Type", "application/json")
}
// Create client with timeout and TLS skip verify
client := &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
// Execute request with retry logic (3 attempts, exponential backoff)
var resp *http.Response
ctx := context.Background()
retryCfg := retry.Config{
MaxAttempts: 3,
InitialDelay: 200 * time.Millisecond,
MaxDelay: 2 * time.Second,
Multiplier: 2.0,
}
err = retry.Do(ctx, retryCfg, func() error {
var reqErr error
// Need to recreate body for retries since it may have been consumed
if bodyStr != "undefined" && bodyStr != "" {
req.Body = io.NopCloser(bytes.NewBufferString(bodyStr))
}
resp, reqErr = client.Do(req)
if reqErr != nil {
return retry.Retryable(reqErr)
}
// Retry on server errors (5xx)
if resp.StatusCode >= 500 {
_ = resp.Body.Close()
return retry.Retryable(fmt.Errorf("server error: %d", resp.StatusCode))
}
return nil
})
if err != nil {
logger.Get().Warn("http_post: request failed after retries", zap.String("url", urlStr), zap.Error(err))
return vf.vm.ToValue(map[string]interface{}{
"status_code": 0,
"error": err.Error(),
"message": "error",
"response_headers": map[string]string{},
"response_body": "",
"content_length": 0,
"response_time_ms": time.Since(start).Milliseconds(),
})
}
defer func() { _ = resp.Body.Close() }()
// Read response body
respBody, err := io.ReadAll(resp.Body)
if err != nil {
logger.Get().Warn("http_post: failed to read response body", zap.String("url", urlStr), zap.Error(err))
return vf.vm.ToValue(map[string]interface{}{
"status_code": resp.StatusCode,
"error": err.Error(),
"message": "error",
"response_headers": map[string]string{},
"response_body": "",
"content_length": 0,
"response_time_ms": time.Since(start).Milliseconds(),
})
}
// Extract response headers
respHeaders := make(map[string]string)
for k, v := range resp.Header {
if len(v) > 0 {
respHeaders[k] = v[0]
}
}
elapsed := time.Since(start).Milliseconds()
logger.Get().Debug(terminal.HiGreen("http_post")+" result", zap.String("url", urlStr), zap.Int("status_code", resp.StatusCode), zap.Int("bodySize", len(respBody)), zap.Int64("response_time_ms", elapsed))
return vf.vm.ToValue(map[string]interface{}{
"status_code": resp.StatusCode,
"error": nil,
"message": "success",
"response_headers": respHeaders,
"response_body": string(respBody),
"content_length": len(respBody),
"response_time_ms": elapsed,
})
}
// randomString generates a random alphanumeric string of given length
// Usage: randomString(length)
// Default length is 16 if not specified or invalid
func (vf *vmFunc) randomString(call goja.FunctionCall) goja.Value {
length := call.Argument(0).ToInteger()
logger.Get().Debug("Calling "+terminal.HiGreen("randomString"), zap.Int64("length", length))
if length <= 0 {
length = 16
}
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
rng := mrand.New(mrand.NewSource(time.Now().UnixNano()))
b := make([]byte, length)
for i := range b {
b[i] = charset[rng.Intn(len(charset))]
}
logger.Get().Debug(terminal.HiGreen("randomString")+" result", zap.Int64("length", length))
return vf.vm.ToValue(string(b))
}
// uuidFunc generates a UUID v4
// Usage: uuid()
func (vf *vmFunc) uuidFunc(call goja.FunctionCall) goja.Value {
logger.Get().Debug("Calling " + terminal.HiGreen("uuid"))
id := uuid.New()
logger.Get().Debug(terminal.HiGreen("uuid")+" result", zap.String("uuid", id.String()))
return vf.vm.ToValue(id.String())
}
// base64Encode encodes a string to base64
// Usage: base64Encode(str)
func (vf *vmFunc) base64Encode(call goja.FunctionCall) goja.Value {
s := call.Argument(0).String()
logger.Get().Debug("Calling "+terminal.HiGreen("base64Encode"), zap.Int("inputLength", len(s)))
if s == "undefined" {
logger.Get().Warn("base64Encode: undefined input")
return vf.vm.ToValue("")
}
encoded := base64.StdEncoding.EncodeToString([]byte(s))
logger.Get().Debug(terminal.HiGreen("base64Encode")+" result", zap.Int("outputLength", len(encoded)))
return vf.vm.ToValue(encoded)
}
// base64Decode decodes a base64 string
// Usage: base64Decode(str)
func (vf *vmFunc) base64Decode(call goja.FunctionCall) goja.Value {
s := call.Argument(0).String()
logger.Get().Debug("Calling "+terminal.HiGreen("base64Decode"), zap.Int("inputLength", len(s)))
if s == "undefined" {
logger.Get().Warn("base64Decode: undefined input")
return vf.vm.ToValue("")
}
decoded, err := base64.StdEncoding.DecodeString(s)
if err != nil {
logger.Get().Warn("base64Decode: failed to decode", zap.Error(err))
return vf.vm.ToValue("")
}
logger.Get().Debug(terminal.HiGreen("base64Decode")+" result", zap.Int("outputLength", len(decoded)))
return vf.vm.ToValue(string(decoded))
}
// removeFile removes a file
// Usage: removeFile(path) -> returns true on success, false on failure
// Will fail if path is a directory
func (vf *vmFunc) removeFile(call goja.FunctionCall) goja.Value {
path := call.Argument(0).String()
logger.Get().Debug("Calling "+terminal.HiGreen("removeFile"), zap.String("path", path))
if path == "undefined" || path == "" {
logger.Get().Warn("removeFile: empty path provided")
return vf.vm.ToValue(false)
}
// Check if path is a file (not directory)
info, err := os.Stat(path)
if err != nil {
logger.Get().Warn("removeFile: file does not exist", zap.String("path", path), zap.Error(err))
return vf.vm.ToValue(false)
}
if info.IsDir() {
logger.Get().Warn("removeFile: path is a directory, not a file", zap.String("path", path))
return vf.vm.ToValue(false)
}
err = os.Remove(path)
if err != nil {
logger.Get().Warn("removeFile: failed to remove file", zap.String("path", path), zap.Error(err))
} else {
logger.Get().Debug(terminal.HiGreen("removeFile")+" result", zap.String("path", path), zap.Bool("success", true))
}
return vf.vm.ToValue(err == nil)
}
// removeFolder removes a folder and all its contents
// Usage: removeFolder(path) -> returns true on success, false on failure
// Will fail if path is not a directory
func (vf *vmFunc) removeFolder(call goja.FunctionCall) goja.Value {
path := call.Argument(0).String()
logger.Get().Debug("Calling "+terminal.HiGreen("removeFolder"), zap.String("path", path))
if path == "undefined" || path == "" {
logger.Get().Warn("removeFolder: empty path provided")
return vf.vm.ToValue(false)
}
// Check if path is a directory
info, err := os.Stat(path)
if err != nil {
logger.Get().Warn("removeFolder: path does not exist", zap.String("path", path), zap.Error(err))
return vf.vm.ToValue(false)
}
if !info.IsDir() {
logger.Get().Warn("removeFolder: path is not a directory", zap.String("path", path))
return vf.vm.ToValue(false)
}
err = os.RemoveAll(path)
if err != nil {
logger.Get().Warn("removeFolder: failed to remove folder", zap.String("path", path), zap.Error(err))
} else {
logger.Get().Debug(terminal.HiGreen("removeFolder")+" result", zap.String("path", path), zap.Bool("success", true))
}
return vf.vm.ToValue(err == nil)
}
func (vf *vmFunc) rmRF(call goja.FunctionCall) goja.Value {
path := call.Argument(0).String()
logger.Get().Debug("Calling "+terminal.HiGreen("rm_rf"), zap.String("path", path))
if path == "undefined" || path == "" {
logger.Get().Warn("rm_rf: empty path provided")
return vf.vm.ToValue(false)
}
err := os.RemoveAll(path)
if err != nil {
logger.Get().Warn("rm_rf: failed to remove path", zap.String("path", path), zap.Error(err))
} else {
logger.Get().Debug(terminal.HiGreen("rm_rf")+" result", zap.String("path", path), zap.Bool("success", true))
}
return vf.vm.ToValue(err == nil)
}
func (vf *vmFunc) removeAllExcept(call goja.FunctionCall) goja.Value {
folder := call.Argument(0).String()
keep := filepath.FromSlash(call.Argument(1).String())
logger.Get().Debug("Calling "+terminal.HiGreen("remove_all_except"), zap.String("folder", folder), zap.String("keep", keep))
if folder == "undefined" || folder == "" || keep == "undefined" || keep == "" {
logger.Get().Warn("remove_all_except: empty folder or keep provided")
return vf.vm.ToValue(false)
}
folderAbs := filepath.Clean(folder)
info, err := os.Stat(folderAbs)
if err != nil {
logger.Get().Warn("remove_all_except: folder does not exist", zap.String("folder", folderAbs), zap.Error(err))
return vf.vm.ToValue(false)
}
if !info.IsDir() {
logger.Get().Warn("remove_all_except: folder is not a directory", zap.String("folder", folderAbs))
return vf.vm.ToValue(false)
}
keepAbs := keep
if !filepath.IsAbs(keepAbs) {
keepAbs = filepath.Join(folderAbs, keepAbs)
}
keepAbs = filepath.Clean(keepAbs)
rel, err := filepath.Rel(folderAbs, keepAbs)
if err != nil {
logger.Get().Warn("remove_all_except: failed to compute relative keep path", zap.String("folder", folderAbs), zap.String("keep", keepAbs), zap.Error(err))
return vf.vm.ToValue(false)
}
if rel == "." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) || rel == ".." {
logger.Get().Warn("remove_all_except: keep path must be inside folder", zap.String("folder", folderAbs), zap.String("keep", keepAbs))
return vf.vm.ToValue(false)
}
keepInfo, err := os.Stat(keepAbs)
if err != nil {
logger.Get().Warn("remove_all_except: keep file does not exist", zap.String("keep", keepAbs), zap.Error(err))
return vf.vm.ToValue(false)
}
if keepInfo.IsDir() {
logger.Get().Warn("remove_all_except: keep path is a directory", zap.String("keep", keepAbs))
return vf.vm.ToValue(false)
}
parts := strings.Split(rel, string(os.PathSeparator))
if len(parts) == 0 {
return vf.vm.ToValue(false)
}
if err := pruneExcept(folderAbs, parts); err != nil {
logger.Get().Warn("remove_all_except: prune failed", zap.String("folder", folderAbs), zap.String("keep", keepAbs), zap.Error(err))
return vf.vm.ToValue(false)
}
return vf.vm.ToValue(true)
}
func pruneExcept(dir string, keepRelParts []string) error {
entries, err := os.ReadDir(dir)
if err != nil {
return err
}
if len(keepRelParts) == 0 {
return nil
}
keepName := keepRelParts[0]
for _, entry := range entries {
name := entry.Name()
fullPath := filepath.Join(dir, name)
if name != keepName {
if err := os.RemoveAll(fullPath); err != nil {
return err
}
continue
}
if len(keepRelParts) > 1 {
if !entry.IsDir() {
return fmt.Errorf("keep path parent is not a directory: %s", fullPath)
}
if err := pruneExcept(fullPath, keepRelParts[1:]); err != nil {
return err
}
}
}
return nil
}
// printf prints a message to stdout and returns the message
// Usage: printf(message) -> string
func (vf *vmFunc) printf(call goja.FunctionCall) goja.Value {
msg := call.Argument(0).String()
logger.Get().Debug("Calling "+terminal.HiGreen("printf"), zap.Int("msgLength", len(msg)))
if msg != "undefined" {
fmt.Println(msg)
}
return vf.vm.ToValue(msg)
}
// catFile prints file content to stdout and returns the content
// Usage: cat_file(path) -> string
func (vf *vmFunc) catFile(call goja.FunctionCall) goja.Value {
path := call.Argument(0).String()
logger.Get().Debug("Calling "+terminal.HiGreen("catFile"), zap.String("path", path))
if path == "undefined" || path == "" {
logger.Get().Warn("catFile: empty path provided")
return vf.vm.ToValue("")
}
content, err := os.ReadFile(path)
if err != nil {
logger.Get().Warn("catFile: failed to read file", zap.String("path", path), zap.Error(err))
return vf.vm.ToValue("")
}
logger.Get().Debug(terminal.HiGreen("catFile")+" result", zap.String("path", path), zap.Int("bytes", len(content)))
fmt.Print(string(content))
return vf.vm.ToValue(string(content))
}
// exit exits the scan with the given code
// Usage: exit(code) - 0 for success, non-zero for error
func (vf *vmFunc) exit(call goja.FunctionCall) goja.Value {
code := call.Argument(0).ToInteger()
logger.Get().Debug("Calling "+terminal.HiGreen("exit"), zap.Int64("code", code))
os.Exit(int(code))
return goja.Undefined()
}
// skip stops execution of the remaining steps in the current module.
// The flow continues to the next module. Accepts an optional message.
// Usage: skip(message?) -> void
func (vf *vmFunc) skip(call goja.FunctionCall) goja.Value {
msg := call.Argument(0).String()
if msg == "undefined" || msg == "" {
msg = "skip() called"
}
logger.Get().Info("Module skip requested: " + msg)
panic(vf.vm.NewGoError(&SkipModuleError{Message: msg}))
}
// execCmd executes a bash command and returns the stdout output
// Usage: exec_cmd(command) -> string
func (vf *vmFunc) execCmd(call goja.FunctionCall) goja.Value {
command := call.Argument(0).String()
logger.Get().Debug("Calling "+terminal.HiGreen("execCmd"), zap.String("command", command))
if command == "undefined" || command == "" {
logger.Get().Warn("execCmd: empty command provided")
return vf.vm.ToValue("")
}
// @NOTE: This is intentional - exec_cmd() is a utility function exposed to workflow
// definitions for executing shell commands. Input comes from trusted workflow YAML files.
cmd := exec.Command("sh", "-c", command)
output, err := cmd.Output()
if err != nil {
logger.Get().Warn("execCmd: command failed", zap.String("command", command), zap.Error(err))
return vf.vm.ToValue("")
}
logger.Get().Debug(terminal.HiGreen("execCmd")+" result", zap.String("command", command), zap.Int("outputLength", len(output)))
return vf.vm.ToValue(strings.TrimSpace(string(output)))
}
func (vf *vmFunc) bash(call goja.FunctionCall) goja.Value {
return vf.execCmd(call)
}
// pythonRunner holds the detected Python binary and whether it's uv.
type pythonRunner struct {
bin string // "uv", "python3", or "python"
isUV bool
}
// findPythonRunner returns the preferred Python runner: uv → python3 → python.
func findPythonRunner() pythonRunner {
if _, err := exec.LookPath("uv"); err == nil {
return pythonRunner{bin: "uv", isUV: true}
}
if _, err := exec.LookPath("python3"); err == nil {
return pythonRunner{bin: "python3"}
}
return pythonRunner{bin: "python"}
}
// execPython runs inline Python code. Prefers `uv run -` (stdin), falls back to `python3 -c`.
// Usage: exec_python(code) -> string
func (vf *vmFunc) execPython(call goja.FunctionCall) goja.Value {
code := call.Argument(0).String()
logger.Get().Debug("Calling "+terminal.HiGreen("execPython"), zap.Int("codeLength", len(code)))
if code == "undefined" || code == "" {
logger.Get().Warn("execPython: empty code provided")
return vf.vm.ToValue("")
}
runner := findPythonRunner()
// @NOTE: This is intentional - exec_python() is a utility function exposed to workflow
// definitions for executing Python code. Input comes from trusted workflow YAML files.
var cmd *exec.Cmd
if runner.isUV {
cmd = exec.Command("uv", "run", "-")
cmd.Stdin = strings.NewReader(code)
} else {
cmd = exec.Command(runner.bin, "-c", code)
}
output, err := cmd.Output()
if err != nil {
logger.Get().Warn("execPython: command failed", zap.String("runner", runner.bin), zap.Error(err))
return vf.vm.ToValue("")
}
logger.Get().Debug(terminal.HiGreen("execPython")+" result", zap.Int("outputLength", len(output)))
return vf.vm.ToValue(strings.TrimSpace(string(output)))
}
// execPythonFile runs a Python file. Prefers `uv run <path>`, falls back to `python3 <path>`.
// Usage: exec_python_file(path) -> string
func (vf *vmFunc) execPythonFile(call goja.FunctionCall) goja.Value {
path := call.Argument(0).String()
logger.Get().Debug("Calling "+terminal.HiGreen("execPythonFile"), zap.String("path", path))
if path == "undefined" || path == "" {
logger.Get().Warn("execPythonFile: empty path provided")
return vf.vm.ToValue("")
}
runner := findPythonRunner()
// @NOTE: This is intentional - exec_python_file() is a utility function exposed to workflow
// definitions for executing Python files. Input comes from trusted workflow YAML files.
var cmd *exec.Cmd
if runner.isUV {
cmd = exec.Command("uv", "run", path)
} else {
cmd = exec.Command(runner.bin, path)
}
output, err := cmd.Output()
if err != nil {
logger.Get().Warn("execPythonFile: command failed", zap.String("runner", runner.bin), zap.String("path", path), zap.Error(err))
return vf.vm.ToValue("")
}
logger.Get().Debug(terminal.HiGreen("execPythonFile")+" result", zap.String("path", path), zap.Int("outputLength", len(output)))
return vf.vm.ToValue(strings.TrimSpace(string(output)))
}
// findBunBin returns "bun" if available in PATH, empty string otherwise.
func findBunBin() string {
if _, err := exec.LookPath("bun"); err == nil {
return "bun"
}
return ""
}
// execTypeScript runs inline TypeScript code via `bun -e '<code>'`.
// Usage: exec_ts(code) -> string
func (vf *vmFunc) execTypeScript(call goja.FunctionCall) goja.Value {
code := call.Argument(0).String()
logger.Get().Debug("Calling "+terminal.HiGreen("execTypeScript"), zap.Int("codeLength", len(code)))
if code == "undefined" || code == "" {
logger.Get().Warn("execTypeScript: empty code provided")
return vf.vm.ToValue("")
}
bunBin := findBunBin()
if bunBin == "" {
logger.Get().Warn("execTypeScript: bun not found in PATH")
return vf.vm.ToValue("")
}
// @NOTE: This is intentional - exec_ts() is a utility function exposed to workflow
// definitions for executing TypeScript code. Input comes from trusted workflow YAML files.
cmd := exec.Command(bunBin, "-e", code)
output, err := cmd.Output()
if err != nil {
logger.Get().Warn("execTypeScript: command failed", zap.Error(err))
return vf.vm.ToValue("")
}
logger.Get().Debug(terminal.HiGreen("execTypeScript")+" result", zap.Int("outputLength", len(output)))
return vf.vm.ToValue(strings.TrimSpace(string(output)))
}
// execTypeScriptFile runs a TypeScript file via `bun run <path>`.
// Usage: exec_ts_file(path) -> string
func (vf *vmFunc) execTypeScriptFile(call goja.FunctionCall) goja.Value {
path := call.Argument(0).String()
logger.Get().Debug("Calling "+terminal.HiGreen("execTypeScriptFile"), zap.String("path", path))
if path == "undefined" || path == "" {
logger.Get().Warn("execTypeScriptFile: empty path provided")
return vf.vm.ToValue("")
}
bunBin := findBunBin()
if bunBin == "" {
logger.Get().Warn("execTypeScriptFile: bun not found in PATH")
return vf.vm.ToValue("")
}
// @NOTE: This is intentional - exec_ts_file() is a utility function exposed to workflow
// definitions for executing TypeScript files. Input comes from trusted workflow YAML files.
cmd := exec.Command(bunBin, "run", path)
output, err := cmd.Output()
if err != nil {
logger.Get().Warn("execTypeScriptFile: command failed", zap.String("path", path), zap.Error(err))
return vf.vm.ToValue("")
}
logger.Get().Debug(terminal.HiGreen("execTypeScriptFile")+" result", zap.String("path", path), zap.Int("outputLength", len(output)))
return vf.vm.ToValue(strings.TrimSpace(string(output)))
}
// commandExists checks if a command is available in PATH
// Usage: commandExists(command) -> bool
func (vf *vmFunc) commandExists(call goja.FunctionCall) goja.Value {
command := call.Argument(0).String()
logger.Get().Debug("Calling "+terminal.HiGreen("commandExists"), zap.String("command", command))
if command == "undefined" || command == "" {
logger.Get().Warn("commandExists: empty command provided")
return vf.vm.ToValue(false)
}
_, err := exec.LookPath(command)
exists := err == nil
logger.Get().Debug(terminal.HiGreen("commandExists")+" result",
zap.String("command", command),
zap.Bool("exists", exists))
return vf.vm.ToValue(exists)
}
// logDebug logs a debug message with [DEBUG] prefix and returns the message
// Usage: log_debug(message) -> string
func (vf *vmFunc) logDebug(call goja.FunctionCall) goja.Value {
msg := call.Argument(0).String()
logger.Get().Debug("Calling "+terminal.HiGreen("logDebug"), zap.Int("msgLength", len(msg)))
if msg != "undefined" {
fmt.Printf("[DEBUG] %s\n", msg)
}
return vf.vm.ToValue(msg)
}
// logInfo logs an info message with [INFO] prefix and returns the message
// Usage: log_info(message) -> string
func (vf *vmFunc) logInfo(call goja.FunctionCall) goja.Value {
msg := call.Argument(0).String()
logger.Get().Debug("Calling "+terminal.HiGreen("logInfo"), zap.Int("msgLength", len(msg)))
if msg != "undefined" {
fmt.Printf("[INFO] %s\n", msg)
}
return vf.vm.ToValue(msg)
}
func (vf *vmFunc) logWarn(call goja.FunctionCall) goja.Value {
msg := call.Argument(0).String()
logger.Get().Debug("Calling "+terminal.HiGreen("logWarn"), zap.Int("msgLength", len(msg)))
if msg != "undefined" {
fmt.Printf("%s %s\n", terminal.Yellow("[WARN]"), msg)
}
return vf.vm.ToValue(msg)
}
func (vf *vmFunc) logError(call goja.FunctionCall) goja.Value {
msg := call.Argument(0).String()
logger.Get().Debug("Calling "+terminal.HiGreen("logError"), zap.Int("msgLength", len(msg)))
if msg != "undefined" {
fmt.Printf("%s %s\n", terminal.Red("[ERROR]"), msg)
}
return vf.vm.ToValue(msg)
}
// printGreen prints a message in green color and returns the message
// Usage: print_green(message) -> string
func (vf *vmFunc) printGreen(call goja.FunctionCall) goja.Value {
msg := call.Argument(0).String()
logger.Get().Debug("Calling "+terminal.HiGreen("printGreen"), zap.Int("msgLength", len(msg)))
if msg != "undefined" {
fmt.Println(terminal.Green(msg))
}
return vf.vm.ToValue(msg)
}
// printBlue prints a message in blue color and returns the message
// Usage: print_blue(message) -> string
func (vf *vmFunc) printBlue(call goja.FunctionCall) goja.Value {
msg := call.Argument(0).String()
logger.Get().Debug("Calling "+terminal.HiGreen("printBlue"), zap.Int("msgLength", len(msg)))
if msg != "undefined" {
fmt.Println(terminal.Blue(msg))
}
return vf.vm.ToValue(msg)
}
// printYellow prints a message in yellow color and returns the message
// Usage: print_yellow(message) -> string
func (vf *vmFunc) printYellow(call goja.FunctionCall) goja.Value {
msg := call.Argument(0).String()
logger.Get().Debug("Calling "+terminal.HiGreen("printYellow"), zap.Int("msgLength", len(msg)))
if msg != "undefined" {
fmt.Println(terminal.Yellow(msg))
}
return vf.vm.ToValue(msg)
}
// printRed prints a message in red color and returns the message
// Usage: print_red(message) -> string
func (vf *vmFunc) printRed(call goja.FunctionCall) goja.Value {
msg := call.Argument(0).String()
logger.Get().Debug("Calling "+terminal.HiGreen("printRed"), zap.Int("msgLength", len(msg)))
if msg != "undefined" {
fmt.Println(terminal.Red(msg))
}
return vf.vm.ToValue(msg)
}
// setVar sets a runtime variable that can be retrieved with get_var
// Also sets the variable on the VM for immediate access in the same execution
// Usage: set_var(name, value) -> string (returns the value)
func (vf *vmFunc) setVar(call goja.FunctionCall) goja.Value {
name := call.Argument(0).String()
value := call.Argument(1).String()
logger.Get().Debug("Calling "+terminal.HiGreen("setVar"), zap.String("name", name), zap.String("value", value))
if name == "undefined" || name == "" {
logger.Get().Warn("setVar: name is required")
return vf.vm.ToValue("")
}
// Handle undefined value as empty string
if value == "undefined" {
value = ""
}
ctx := vf.getContext()
if ctx != nil {
if ctx.RuntimeVars == nil {
ctx.RuntimeVars = make(map[string]string)
}
ctx.RuntimeVars[name] = value
// Also set on VM for immediate access in same execution
_ = vf.vm.Set(name, value)
}
logger.Get().Debug(terminal.HiGreen("setVar")+" result", zap.String("name", name), zap.String("value", value))
return vf.vm.ToValue(value)
}
// getVar retrieves a runtime variable set with set_var
// Usage: get_var(name) -> string (returns empty string if not found)
func (vf *vmFunc) getVar(call goja.FunctionCall) goja.Value {
name := call.Argument(0).String()
logger.Get().Debug("Calling "+terminal.HiGreen("getVar"), zap.String("name", name))
if name == "undefined" || name == "" {
logger.Get().Warn("getVar: name is required")
return vf.vm.ToValue("")
}
ctx := vf.getContext()
if ctx != nil && ctx.RuntimeVars != nil {
if val, ok := ctx.RuntimeVars[name]; ok {
logger.Get().Debug(terminal.HiGreen("getVar")+" result", zap.String("name", name), zap.String("value", val))
return vf.vm.ToValue(val)
}
}
logger.Get().Debug(terminal.HiGreen("getVar")+" result (not found)", zap.String("name", name))
return vf.vm.ToValue("")
}
// sleep pauses execution for the given number of seconds
// Usage: sleep(seconds) -> void
func (vf *vmFunc) sleep(call goja.FunctionCall) goja.Value {
seconds := call.Argument(0).ToInteger()
logger.Get().Debug("Calling "+terminal.HiGreen("sleep"), zap.Int64("seconds", seconds))
if seconds <= 0 {
seconds = 1
}
time.Sleep(time.Duration(seconds) * time.Second)
logger.Get().Debug(terminal.HiGreen("sleep")+" completed", zap.Int64("seconds", seconds))
return goja.Undefined()
}
// saveContent saves content to a file
// Usage: save_content(content, path) -> bool
func (vf *vmFunc) saveContent(call goja.FunctionCall) goja.Value {
content := call.Argument(0).String()
path := call.Argument(1).String()
logger.Get().Debug("Calling "+terminal.HiGreen("saveContent"), zap.String("path", path), zap.Int("contentLength", len(content)))
if path == "undefined" || path == "" {
logger.Get().Warn("saveContent: empty path provided")
return vf.vm.ToValue(false)
}
// Handle undefined content as empty string
if content == "undefined" {
content = ""
}
// Ensure parent directory exists
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0755); err != nil {
logger.Get().Warn("saveContent: failed to create directory", zap.String("dir", dir), zap.Error(err))
return vf.vm.ToValue(false)
}
// Write content to file
err := os.WriteFile(path, []byte(content), 0644)
if err != nil {
logger.Get().Warn("saveContent: failed to write file", zap.String("path", path), zap.Error(err))
} else {
logger.Get().Debug(terminal.HiGreen("saveContent")+" result", zap.String("path", path), zap.Bool("success", true))
}
return vf.vm.ToValue(err == nil)
}
// moveFile moves a file from source to destination
// Uses os.Rename for same-filesystem moves (instant),
// falls back to buffered copy+delete for cross-filesystem moves
// Usage: moveFile(source, dest) -> bool
func (vf *vmFunc) moveFile(call goja.FunctionCall) goja.Value {
source := call.Argument(0).String()
dest := call.Argument(1).String()
logger.Get().Debug("Calling "+terminal.HiGreen("moveFile"),
zap.String("source", source), zap.String("dest", dest))
// Validate arguments
if source == "undefined" || source == "" || dest == "undefined" || dest == "" {
logger.Get().Warn("moveFile: empty source or dest provided")
return vf.vm.ToValue(false)
}
// Check source exists and is a file
info, err := os.Stat(source)
if err != nil {
logger.Get().Warn("moveFile: source does not exist",
zap.String("source", source), zap.Error(err))
return vf.vm.ToValue(false)
}
if info.IsDir() {
logger.Get().Warn("moveFile: source is a directory, not a file",
zap.String("source", source))
return vf.vm.ToValue(false)
}
// Ensure destination directory exists
if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil {
logger.Get().Warn("moveFile: failed to create destination directory",
zap.String("dest", dest), zap.Error(err))
return vf.vm.ToValue(false)
}
// Try atomic rename first (fast, same filesystem)
err = os.Rename(source, dest)
if err == nil {
logger.Get().Debug(terminal.HiGreen("moveFile")+" result (renamed)",
zap.String("source", source), zap.String("dest", dest), zap.Bool("success", true))
return vf.vm.ToValue(true)
}
// Fallback: buffered copy + delete (cross-filesystem)
if err := copyFileBuffered(source, dest, info.Mode()); err != nil {
logger.Get().Warn("moveFile: failed to copy file",
zap.String("source", source), zap.String("dest", dest), zap.Error(err))
return vf.vm.ToValue(false)
}
// Remove source after successful copy
if err := os.Remove(source); err != nil {
logger.Get().Warn("moveFile: copied but failed to remove source",
zap.String("source", source), zap.Error(err))
// Still return true since the file was successfully moved
}
logger.Get().Debug(terminal.HiGreen("moveFile")+" result (copied)",
zap.String("source", source), zap.String("dest", dest), zap.Bool("success", true))
return vf.vm.ToValue(true)
}
// snapshotExport exports a workspace as a ZIP snapshot by running osmedeus snapshot export as a subprocess.
// Usage: snapshot_export(workspace, dest?) -> string (zip path on success, empty on failure)
func (vf *vmFunc) snapshotExport(call goja.FunctionCall) goja.Value {
workspace := call.Argument(0).String()
dest := call.Argument(1).String()
if workspace == "undefined" || workspace == "" {
logger.Get().Warn("snapshot_export: workspace is required")
return vf.vm.ToValue("")
}
if dest == "undefined" {
dest = ""
}
// Find the current executable
exePath, err := os.Executable()
if err != nil {
logger.Get().Warn("snapshot_export: failed to find executable", zap.Error(err))
return vf.vm.ToValue("")
}
// Build command arguments
args := []string{"snapshot", "export", workspace}
if dest != "" {
args = append(args, "-o", dest)
}
logger.Get().Debug("Calling "+terminal.HiGreen("snapshot_export"),
zap.String("workspace", workspace),
zap.String("dest", dest))
// @NOTE: This is intentional - snapshot_export() is a utility function exposed to workflow
// definitions for exporting workspaces. Input comes from trusted workflow YAML files.
cmd := exec.Command(exePath, args...)
output, err := cmd.CombinedOutput()
if err != nil {
logger.Get().Warn("snapshot_export: command failed",
zap.String("workspace", workspace),
zap.Error(err),
zap.String("output", string(output)))
return vf.vm.ToValue("")
}
// Parse output to extract the zip file path from "File: <path>" line
var zipPath string
for _, line := range strings.Split(string(output), "\n") {
line = strings.TrimSpace(line)
if idx := strings.Index(line, "File:"); idx >= 0 {
zipPath = strings.TrimSpace(line[idx+len("File:"):])
break
}
}
logger.Get().Debug(terminal.HiGreen("snapshot_export")+" result",
zap.String("workspace", workspace),
zap.String("zipPath", zipPath))
return vf.vm.ToValue(zipPath)
}
// snapshotImport imports a workspace from a ZIP snapshot by running osmedeus snapshot import as a subprocess.
// Usage: snapshot_import(source) -> string (workspace name on success, empty on failure)
func (vf *vmFunc) snapshotImport(call goja.FunctionCall) goja.Value {
source := call.Argument(0).String()
if source == "undefined" || source == "" {
logger.Get().Warn("snapshot_import: source is required")
return vf.vm.ToValue("")
}
// Find the current executable
exePath, err := os.Executable()
if err != nil {
logger.Get().Warn("snapshot_import: failed to find executable", zap.Error(err))
return vf.vm.ToValue("")
}
// Build command arguments with --force to skip interactive confirmation
args := []string{"snapshot", "import", source, "--force"}
logger.Get().Debug("Calling "+terminal.HiGreen("snapshot_import"),
zap.String("source", source))
// @NOTE: This is intentional - snapshot_import() is a utility function exposed to workflow
// definitions for importing workspaces. Input comes from trusted workflow YAML files.
cmd := exec.Command(exePath, args...)
output, err := cmd.CombinedOutput()
if err != nil {
logger.Get().Warn("snapshot_import: command failed",
zap.String("source", source),
zap.Error(err),
zap.String("output", string(output)))
return vf.vm.ToValue("")
}
// Parse output to extract workspace name from "Workspace: <name>" line
var workspaceName string
for _, line := range strings.Split(string(output), "\n") {
line = strings.TrimSpace(line)
if idx := strings.Index(line, "Workspace:"); idx >= 0 {
workspaceName = strings.TrimSpace(line[idx+len("Workspace:"):])
break
}
}
logger.Get().Debug(terminal.HiGreen("snapshot_import")+" result",
zap.String("source", source),
zap.String("workspace", workspaceName))
return vf.vm.ToValue(workspaceName)
}
// parseParamsToFlags parses a comma-separated "key=value" string into -p flags.
// E.g. "threads=10,deep=true" -> ["-p", "threads=10", "-p", "deep=true"]
func parseParamsToFlags(params string) []string {
params = strings.TrimSpace(params)
if params == "" || params == "undefined" {
return nil
}
var flags []string
for _, pair := range strings.Split(params, ",") {
pair = strings.TrimSpace(pair)
if pair == "" || !strings.Contains(pair, "=") {
continue
}
flags = append(flags, "-p", pair)
}
return flags
}
// runOsmedeus executes the current osmedeus binary with the given flag (-m or -f), name, target, and optional params.
// Returns the combined stdout+stderr output as a string.
func (vf *vmFunc) runOsmedeus(flag, name, target, params, funcName string) goja.Value {
// Find the current executable
exePath, err := os.Executable()
if err != nil {
logger.Get().Warn(funcName+": failed to find executable", zap.Error(err))
return vf.vm.ToValue("")
}
// Build command arguments
args := []string{"run", flag, name, "-t", target}
args = append(args, parseParamsToFlags(params)...)
logger.Get().Debug("Calling "+terminal.HiGreen(funcName),
zap.String("exe", exePath),
zap.Strings("args", args))
// @NOTE: This is intentional - run_module/run_flow are utility functions exposed to workflow
// definitions for launching sub-scans. Input comes from trusted workflow YAML files.
cmd := exec.Command(exePath, args...)
output, err := cmd.CombinedOutput()
if err != nil {
logger.Get().Warn(funcName+": command failed",
zap.String("name", name),
zap.String("target", target),
zap.Error(err),
zap.String("output", string(output)))
}
logger.Get().Debug(terminal.HiGreen(funcName)+" result",
zap.String("name", name),
zap.String("target", target),
zap.Int("outputLength", len(output)))
return vf.vm.ToValue(strings.TrimSpace(string(output)))
}
// runModule runs an osmedeus module as a subprocess
// Usage: run_module(module, target, params?) -> string
// params is optional comma-separated key=value pairs: "threads=10,deep=true"
func (vf *vmFunc) runModule(call goja.FunctionCall) goja.Value {
module := call.Argument(0).String()
target := call.Argument(1).String()
params := call.Argument(2).String()
if module == "undefined" || module == "" || target == "undefined" || target == "" {
logger.Get().Warn("run_module: module and target are required")
return vf.vm.ToValue("")
}
return vf.runOsmedeus("-m", module, target, params, "run_module")
}
// runFlow runs an osmedeus flow as a subprocess
// Usage: run_flow(flow, target, params?) -> string
// params is optional comma-separated key=value pairs: "threads=10,deep=true"
func (vf *vmFunc) runFlow(call goja.FunctionCall) goja.Value {
flow := call.Argument(0).String()
target := call.Argument(1).String()
params := call.Argument(2).String()
if flow == "undefined" || flow == "" || target == "undefined" || target == "" {
logger.Get().Warn("run_flow: flow and target are required")
return vf.vm.ToValue("")
}
return vf.runOsmedeus("-f", flow, target, params, "run_flow")
}
// runAgent runs an ACP agent subprocess and returns its output.
// Usage: run_agent(message, agent_name?) -> string
// agent_name defaults to "claude-code" if not provided.
// Uses the registered RunAgentFunc hook to avoid circular imports with the executor package.
func (vf *vmFunc) runAgent(call goja.FunctionCall) goja.Value {
message := call.Argument(0).String()
agentName := call.Argument(1).String()
if message == "undefined" || message == "" {
logger.Get().Warn("run_agent: message is required")
return vf.vm.ToValue("")
}
if agentName == "undefined" {
agentName = ""
}
logger.Get().Debug("Calling "+terminal.HiGreen("run_agent"),
zap.String("message_length", fmt.Sprintf("%d", len(message))),
zap.String("agent", agentName))
fn := GetRunAgentFunc()
if fn == nil {
logger.Get().Warn("run_agent: agent hook not registered (call functions.RegisterRunAgentFunc first)")
return vf.vm.ToValue("")
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
defer cancel()
output, _, err := fn(ctx, message, agentName)
if err != nil {
logger.Get().Warn("run_agent failed", zap.Error(err))
return vf.vm.ToValue("")
}
return vf.vm.ToValue(output)
}
// runOnMaster executes an action on the master node in distributed mode.
// In distributed mode, the request is sent via Redis to the master.
// In standalone mode, falls back to local execution.
//
// Usage:
//
// run_on_master('func', 'db_import_sarif("ws", "/path/file.sarif")') -> bool
// run_on_master('run', 'subdomain', 'example.com', 'threads=10') -> bool
// run_on_master('bash', 'nmap -sV target.com') -> bool
func (vf *vmFunc) runOnMaster(call goja.FunctionCall) goja.Value {
action := call.Argument(0).String()
if action == "undefined" || action == "" {
logger.Get().Warn("run_on_master: action is required (func, run, or bash)")
return vf.vm.ToValue(false)
}
log := logger.Get()
switch action {
case "func":
expr := call.Argument(1).String()
if expr == "undefined" || expr == "" {
log.Warn("run_on_master: expression is required for 'func' action")
return vf.vm.ToValue(false)
}
log.Debug("Calling "+terminal.HiGreen("run_on_master"),
zap.String("action", "func"),
zap.String("expr", expr))
// Try distributed path first
if trySendExecuteRequest("func", expr, "", "", "", "master", "") {
log.Debug("run_on_master: sent func request to master via Redis")
return vf.vm.ToValue(true)
}
// Fallback: execute locally using the current runtime
log.Warn("run_on_master: not in distributed mode, executing locally",
zap.String("action", action),
zap.String("hint", "ensure worker mode is active and Redis is configured"))
execCtx := make(map[string]interface{})
_, err := vf.runtime.Execute(expr, execCtx)
if err != nil {
log.Warn("run_on_master: local execution failed",
zap.String("expr", expr),
zap.Error(err))
return vf.vm.ToValue(false)
}
return vf.vm.ToValue(true)
case "run":
workflow := call.Argument(1).String()
target := call.Argument(2).String()
params := call.Argument(3).String()
if workflow == "undefined" || workflow == "" || target == "undefined" || target == "" {
log.Warn("run_on_master: workflow and target are required for 'run' action")
return vf.vm.ToValue(false)
}
if params == "undefined" {
params = ""
}
log.Debug("Calling "+terminal.HiGreen("run_on_master"),
zap.String("action", "run"),
zap.String("workflow", workflow),
zap.String("target", target),
zap.String("params", params))
// Try distributed path first
if trySendExecuteRequest("run", "", workflow, target, params, "master", "") {
log.Debug("run_on_master: sent run request to master via Redis")
return vf.vm.ToValue(true)
}
// Fallback: run locally as subprocess
log.Warn("run_on_master: not in distributed mode, running locally",
zap.String("action", action),
zap.String("hint", "ensure worker mode is active and Redis is configured"))
vf.runOsmedeus("-m", workflow, target, params, "run_on_master")
return vf.vm.ToValue(true)
case "bash":
command := call.Argument(1).String()
if command == "undefined" || command == "" {
log.Warn("run_on_master: command is required for 'bash' action")
return vf.vm.ToValue(false)
}
log.Debug("Calling "+terminal.HiGreen("run_on_master"),
zap.String("action", "bash"),
zap.String("command", command))
// Try distributed path first
if trySendExecuteRequest("bash", command, "", "", "", "master", "") {
log.Debug("run_on_master: sent bash request to master via Redis")
return vf.vm.ToValue(true)
}
// Fallback: execute locally
log.Warn("run_on_master: not in distributed mode, executing bash locally")
// @NOTE: This is intentional - run_on_master('bash', cmd) is a utility function
// exposed to workflow definitions. Input comes from trusted workflow YAML files.
cmd := exec.Command("sh", "-c", command)
output, err := cmd.CombinedOutput()
if err != nil {
log.Warn("run_on_master: local bash execution failed",
zap.String("command", command), zap.Error(err),
zap.String("output", string(output)))
return vf.vm.ToValue(false)
}
return vf.vm.ToValue(true)
default:
log.Warn("run_on_master: unknown action",
zap.String("action", action),
zap.String("valid", "func, run, bash"))
return vf.vm.ToValue(false)
}
}
// runOnWorker executes an action on worker node(s) in distributed mode.
// In distributed mode, the request is routed via Redis through the master to target workers.
// In standalone mode, falls back to local execution.
//
// Usage:
//
// run_on_worker('all', 'func', 'log_info("hello")') -> bool
// run_on_worker('scanner-1', 'run', 'subdomain', 'example.com', 'threads=10') -> bool
// run_on_worker('all', 'bash', 'apt update && apt install -y nmap') -> bool
func (vf *vmFunc) runOnWorker(call goja.FunctionCall) goja.Value {
scope := call.Argument(0).String()
if scope == "undefined" || scope == "" {
scope = "all"
}
action := call.Argument(1).String()
if action == "undefined" || action == "" {
logger.Get().Warn("run_on_worker: action is required (func, run, bash)")
return vf.vm.ToValue(false)
}
log := logger.Get()
switch action {
case "func":
expr := call.Argument(2).String()
if expr == "undefined" || expr == "" {
log.Warn("run_on_worker: expression required for 'func' action")
return vf.vm.ToValue(false)
}
if trySendExecuteRequest("func", expr, "", "", "", "worker", scope) {
return vf.vm.ToValue(true)
}
// Fallback: execute locally
log.Warn("run_on_worker: not in distributed mode, executing locally")
execCtx := make(map[string]interface{})
_, err := vf.runtime.Execute(expr, execCtx)
if err != nil {
log.Warn("run_on_worker: local execution failed", zap.Error(err))
return vf.vm.ToValue(false)
}
return vf.vm.ToValue(true)
case "run":
workflow := call.Argument(2).String()
target := call.Argument(3).String()
params := call.Argument(4).String()
if workflow == "undefined" || workflow == "" || target == "undefined" || target == "" {
log.Warn("run_on_worker: workflow and target required for 'run' action")
return vf.vm.ToValue(false)
}
if params == "undefined" {
params = ""
}
if trySendExecuteRequest("run", "", workflow, target, params, "worker", scope) {
return vf.vm.ToValue(true)
}
// Fallback: run locally
log.Warn("run_on_worker: not in distributed mode, running locally")
vf.runOsmedeus("-m", workflow, target, params, "run_on_worker")
return vf.vm.ToValue(true)
case "bash":
command := call.Argument(2).String()
if command == "undefined" || command == "" {
log.Warn("run_on_worker: command required for 'bash' action")
return vf.vm.ToValue(false)
}
if trySendExecuteRequest("bash", command, "", "", "", "worker", scope) {
return vf.vm.ToValue(true)
}
// Fallback: execute locally
log.Warn("run_on_worker: not in distributed mode, executing bash locally")
// @NOTE: This is intentional - run_on_worker('scope', 'bash', cmd) is a utility function
// exposed to workflow definitions. Input comes from trusted workflow YAML files.
cmd := exec.Command("sh", "-c", command)
_, _ = cmd.CombinedOutput()
return vf.vm.ToValue(true)
default:
log.Warn("run_on_worker: unknown action (expected 'func', 'run', or 'bash'). "+
"Did you forget the scope argument? Correct usage: run_on_worker(scope, action, ...args)",
zap.String("action", action))
return vf.vm.ToValue(false)
}
}
// copyFileBuffered copies a file using buffered I/O (memory-efficient for large files)
func copyFileBuffered(source, dest string, mode os.FileMode) error {
srcFile, err := os.Open(source)
if err != nil {
return err
}
defer func() { _ = srcFile.Close() }()
dstFile, err := os.OpenFile(dest, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
if err != nil {
return err
}
defer func() { _ = dstFile.Close() }()
_, err = io.Copy(dstFile, srcFile)
return err
}