Skip to content

Commit 55b0589

Browse files
author
Gemini CLI
committed
feat: implement OpenClaw Protocol Adapter with API server
1 parent abca771 commit 55b0589

File tree

3 files changed

+161
-20
lines changed

3 files changed

+161
-20
lines changed

cmd/aegisclaw/main.go

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/mackeh/AegisClaw/internal/sandbox"
1616
"github.com/mackeh/AegisClaw/internal/scope"
1717
"github.com/mackeh/AegisClaw/internal/secrets"
18+
"github.com/mackeh/AegisClaw/internal/server"
1819
"github.com/mackeh/AegisClaw/internal/skill"
1920
"github.com/spf13/cobra"
2021
)
@@ -38,6 +39,7 @@ human-in-the-loop approvals, encrypted secrets, and tamper-evident audit logging
3839
rootCmd.AddCommand(logsCmd())
3940
rootCmd.AddCommand(sandboxCmd())
4041
rootCmd.AddCommand(skillsCmd())
42+
rootCmd.AddCommand(serveCmd())
4143

4244
if err := rootCmd.Execute(); err != nil {
4345
fmt.Fprintln(os.Stderr, err)
@@ -128,7 +130,7 @@ func runCmd() *cobra.Command {
128130
cmdName := parts[1]
129131
args := parts[2:]
130132

131-
if err := agent.ExecuteSkill(cmd.Context(), targetManifest, cmdName, args); err != nil {
133+
if _, err := agent.ExecuteSkill(cmd.Context(), targetManifest, cmdName, args); err != nil {
132134
fmt.Printf("❌ Execution failed: %v\n", err)
133135
}
134136
} else {
@@ -392,7 +394,10 @@ func sandboxCmd() *cobra.Command {
392394
return err
393395
}
394396

395-
return agent.ExecuteSkill(cmd.Context(), m, cmdName, userArgs)
397+
if _, err := agent.ExecuteSkill(cmd.Context(), m, cmdName, userArgs); err != nil {
398+
return err
399+
}
400+
return nil
396401
},
397402
})
398403

@@ -439,4 +444,18 @@ func skillsCmd() *cobra.Command {
439444
})
440445

441446
return cmd
442-
}
447+
}
448+
func serveCmd() *cobra.Command {
449+
var port int
450+
cmd := &cobra.Command{
451+
Use: "serve",
452+
Short: "Start the AegisClaw API server",
453+
RunE: func(cmd *cobra.Command, args []string) error {
454+
s := server.NewServer(port)
455+
return s.Start()
456+
},
457+
}
458+
459+
cmd.Flags().IntVarP(&port, "port", "p", 8080, "Port to listen on")
460+
return cmd
461+
}

internal/agent/agent.go

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package agent
22

33
import (
4+
"bytes"
45
"context"
56
"fmt"
67
"io"
@@ -18,12 +19,19 @@ import (
1819
"github.com/mackeh/AegisClaw/internal/skill"
1920
)
2021

22+
// ExecutionResult holds the captured output of a skill run
23+
type ExecutionResult struct {
24+
ExitCode int
25+
Stdout string
26+
Stderr string
27+
}
28+
2129
// ExecuteSkill handles the end-to-end execution of a skill command
22-
func ExecuteSkill(ctx context.Context, m *skill.Manifest, cmdName string, userArgs []string) error {
30+
func ExecuteSkill(ctx context.Context, m *skill.Manifest, cmdName string, userArgs []string) (*ExecutionResult, error) {
2331
// 1. Find Command
2432
skillCmd, ok := m.Commands[cmdName]
2533
if !ok {
26-
return fmt.Errorf("command '%s' not found in skill '%s'", cmdName, m.Name)
34+
return nil, fmt.Errorf("command '%s' not found in skill '%s'", cmdName, m.Name)
2735
}
2836

2937
// 2. Prepare Scopes
@@ -50,7 +58,7 @@ func ExecuteSkill(ctx context.Context, m *skill.Manifest, cmdName string, userAr
5058
// 3. Load Policy & Evaluate
5159
p, err := policy.LoadDefaultPolicy()
5260
if err != nil {
53-
return fmt.Errorf("failed to load policy: %w", err)
61+
return nil, fmt.Errorf("failed to load policy: %w", err)
5462
}
5563
engine := policy.NewEngine(p)
5664
decision, riskyScopes := engine.EvaluateRequest(req)
@@ -61,13 +69,13 @@ func ExecuteSkill(ctx context.Context, m *skill.Manifest, cmdName string, userAr
6169
switch decision {
6270
case policy.Deny:
6371
fmt.Println("❌ Policy DENIED this action.")
64-
return fmt.Errorf("policy denied action")
72+
return nil, fmt.Errorf("policy denied action")
6573

6674
case policy.RequireApproval:
6775
// Check persistent approvals
6876
store, err := approval.NewStore()
6977
if err != nil {
70-
return err
78+
return nil, err
7179
}
7280

7381
allApproved := true
@@ -85,12 +93,12 @@ func ExecuteSkill(ctx context.Context, m *skill.Manifest, cmdName string, userAr
8593
// Prompt User
8694
userDec, err := approval.RequestApproval(req)
8795
if err != nil {
88-
return err
96+
return nil, err
8997
}
9098

9199
if userDec == "deny" {
92100
fmt.Println("❌ User denied the request.")
93-
return fmt.Errorf("user denied request")
101+
return nil, fmt.Errorf("user denied request")
94102
}
95103

96104
finalDecision = "allow"
@@ -119,7 +127,7 @@ func ExecuteSkill(ctx context.Context, m *skill.Manifest, cmdName string, userAr
119127
}
120128

121129
if finalDecision != "allow" {
122-
return fmt.Errorf("execution blocked")
130+
return nil, fmt.Errorf("execution blocked")
123131
}
124132

125133
// 6. Prepare Execution Environment
@@ -128,7 +136,6 @@ func ExecuteSkill(ctx context.Context, m *skill.Manifest, cmdName string, userAr
128136

129137
// Inject Secrets if allowed
130138
if finalDecision == "allow" {
131-
cfgDir, _ := config.DefaultConfigDir()
132139
secretsDir := filepath.Join(cfgDir, "secrets")
133140
mgr := secrets.NewManager(secretsDir)
134141

@@ -144,11 +151,12 @@ func ExecuteSkill(ctx context.Context, m *skill.Manifest, cmdName string, userAr
144151
}
145152
}
146153

154+
// 7. Execute
147155
fmt.Printf("🚀 Running skill: %s\n", m.Name)
148156

149157
exec, err := sandbox.NewDockerExecutor()
150158
if err != nil {
151-
return fmt.Errorf("failed to initialize executor: %w", err)
159+
return nil, fmt.Errorf("failed to initialize executor: %w", err)
152160
}
153161

154162
// Set a default timeout for execution
@@ -163,13 +171,23 @@ func ExecuteSkill(ctx context.Context, m *skill.Manifest, cmdName string, userAr
163171
AllowedDomains: allowedDomains,
164172
})
165173
if err != nil {
166-
return fmt.Errorf("execution failed: %w", err)
174+
return nil, fmt.Errorf("execution failed: %w", err)
167175
}
168176

169-
// Stream output
170-
io.Copy(os.Stdout, result.Stdout)
171-
io.Copy(os.Stderr, result.Stderr)
172-
173-
fmt.Printf("✅ Skill finished (exit code %d)\n", result.ExitCode)
174-
return nil
177+
// Capture output
178+
stdoutBuf := new(bytes.Buffer)
179+
stderrBuf := new(bytes.Buffer)
180+
181+
// We still stream to console for better visibility during 'run' or manual CLI use
182+
stdoutTee := io.TeeReader(result.Stdout, stdoutBuf)
183+
stderrTee := io.TeeReader(result.Stderr, stderrBuf)
184+
185+
io.Copy(os.Stdout, stdoutTee)
186+
io.Copy(os.Stderr, stderrTee)
187+
188+
return &ExecutionResult{
189+
ExitCode: result.ExitCode,
190+
Stdout: stdoutBuf.String(),
191+
Stderr: stderrBuf.String(),
192+
}, nil
175193
}

internal/server/server.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package server
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
"path/filepath"
8+
9+
"github.com/mackeh/AegisClaw/internal/agent"
10+
"github.com/mackeh/AegisClaw/internal/config"
11+
"github.com/mackeh/AegisClaw/internal/skill"
12+
)
13+
14+
// Request represents a skill execution request
15+
type Request struct {
16+
Skill string `json:"skill"`
17+
Command string `json:"command"`
18+
Args []string `json:"args"`
19+
}
20+
21+
// Response represents the result of a skill execution
22+
type Response struct {
23+
ExitCode int `json:"exit_code"`
24+
Stdout string `json:"stdout"`
25+
Stderr string `json:"stderr"`
26+
Error string `json:"error,omitempty"`
27+
}
28+
29+
// Server handles tool execution requests
30+
type Server struct {
31+
Port int
32+
}
33+
34+
func NewServer(port int) *Server {
35+
return &Server{Port: port}
36+
}
37+
38+
func (s *Server) Start() error {
39+
http.HandleFunc("/execute", s.handleExecute)
40+
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
41+
w.WriteHeader(http.StatusOK)
42+
w.Write([]byte("OK"))
43+
})
44+
45+
fmt.Printf("📡 AegisClaw API listening on 127.0.0.1:%d...\n", s.Port)
46+
return http.ListenAndServe(fmt.Sprintf("127.0.0.1:%d", s.Port), nil)
47+
}
48+
49+
func (s *Server) handleExecute(w http.ResponseWriter, r *http.Request) {
50+
if r.Method != http.MethodPost {
51+
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
52+
return
53+
}
54+
55+
var req Request
56+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
57+
http.Error(w, err.Error(), http.StatusBadRequest)
58+
return
59+
}
60+
61+
// 1. Find the skill manifest
62+
cfgDir, _ := config.DefaultConfigDir()
63+
64+
// Check standard locations
65+
searchDirs := []string{
66+
filepath.Join(cfgDir, "skills"),
67+
"skills",
68+
}
69+
70+
var m *skill.Manifest
71+
for _, dir := range searchDirs {
72+
manifestPath := filepath.Join(dir, req.Skill, "skill.yaml")
73+
found, err := skill.LoadManifest(manifestPath)
74+
if err == nil {
75+
m = found
76+
break
77+
}
78+
}
79+
80+
if m == nil {
81+
s.sendResponse(w, http.StatusNotFound, Response{Error: fmt.Sprintf("skill '%s' not found", req.Skill)})
82+
return
83+
}
84+
85+
// 2. Execute
86+
result, err := agent.ExecuteSkill(r.Context(), m, req.Command, req.Args)
87+
if err != nil {
88+
s.sendResponse(w, http.StatusInternalServerError, Response{Error: err.Error()})
89+
return
90+
}
91+
92+
// 3. Return result
93+
s.sendResponse(w, http.StatusOK, Response{
94+
ExitCode: result.ExitCode,
95+
Stdout: result.Stdout,
96+
Stderr: result.Stderr,
97+
})
98+
}
99+
100+
func (s *Server) sendResponse(w http.ResponseWriter, status int, resp Response) {
101+
w.Header().Set("Content-Type", "application/json")
102+
w.WriteHeader(status)
103+
json.NewEncoder(w).Encode(resp)
104+
}

0 commit comments

Comments
 (0)