Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cmd/agent_chat_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ func runClientMode(cfg *config.Config, addr, agentName, message, sessionKey stri
// wsConnect sends the connect RPC and waits for auth response.
func wsConnect(conn *websocket.Conn, token string) error {
params := map[string]string{}
userId := os.Getenv("GOCLAW_USER_ID")
if userId == "" { userId = "system" }
params["user_id"] = userId
if token != "" {
params["token"] = token
}
Expand Down
1 change: 1 addition & 0 deletions cmd/gateway_http_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ func gatewayHTTPDoRaw(method, path string, body any) ([]byte, int, error) {
req.Header.Set("Content-Type", "application/json")
if token := resolveGatewayToken(); token != "" {
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("X-GoClaw-User-Id", "system")
}

resp, err := httpClient.Do(req)
Expand Down
4 changes: 2 additions & 2 deletions internal/gateway/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ func (r *MethodRouter) handleConnect(ctx context.Context, client *Client, req *p
if paired {
client.role = permissions.RoleOperator
client.authenticated = true
client.userID = params.UserID
client.userID = params.UserID
client.pairedSenderID = params.SenderID
client.pairedChannel = "browser"
tid, errCode := r.resolveTenantHint(ctx, params.TenantHint, params.UserID)
Expand Down Expand Up @@ -282,7 +282,7 @@ func (r *MethodRouter) handleConnect(ctx context.Context, client *Client, req *p
// Path 4: Fallback → viewer (wrong token or pairing not available)
client.role = permissions.RoleViewer
client.authenticated = true
client.userID = params.UserID
client.userID = params.UserID
tid, errCode := r.resolveTenantHint(ctx, params.TenantHint, params.UserID)
if errCode != "" {
client.SendResponse(protocol.NewErrorResponse(req.ID, errCode, "tenant access revoked"))
Expand Down
56 changes: 56 additions & 0 deletions internal/providers/acp/acp_gemini_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package acp

import (
"context"
"fmt"
"log/slog"
"os"
"testing"
"time"
)

func TestGeminiProtocolMapping(t *testing.T) {
// Enable debug logging to stderr
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug})))

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

pp := NewProcessPool("gemini", []string{"--acp", "--yolo"}, ".", 10*time.Minute)
defer pp.Close()

proc, err := pp.GetOrSpawn(ctx, "test-session")
if err != nil {
t.Fatalf("Spawn failed: %v", err)
}

sid, err := proc.NewSession(ctx)
if err != nil {
t.Fatalf("NewSession failed: %v", err)
}

fmt.Println("-> Testing Gemini Protocol Mapping...")

var collectedText string
_, err = proc.Prompt(ctx, sid, []ContentBlock{
{Type: "text", Text: "Hello, reply with 'ACP_SUCCESS' and nothing else."},
}, func(su SessionUpdate) {
if su.Message != nil {
for _, b := range su.Message.Content {
if b.Type == "text" {
fmt.Printf("<- MAPPED CHUNK: %s\n", b.Text)
collectedText += b.Text
}
}
}
})

if err != nil {
t.Fatalf("Prompt failed: %v", err)
}

fmt.Printf("== COLLECTED TOTAL: %s ==\n", collectedText)
if collectedText == "" {
t.Error("No text collected via mapped Message field!")
}
}
21 changes: 20 additions & 1 deletion internal/providers/acp/helpers.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,35 @@
package acp

import (
"context"
"io"
"strings"
"sync"
)

// contextKey is the unexported type for context values set by this package.
type contextKey string

const goclawSessionCtxKey contextKey = "goclaw_session"

// WithGoclawSession attaches the goclaw conversation session key to ctx so
// that ACP-level logs can correlate the ACP session ID with the goclaw session.
func WithGoclawSession(ctx context.Context, key string) context.Context {
return context.WithValue(ctx, goclawSessionCtxKey, key)
}

// goclawSessionFromCtx extracts the goclaw session key injected by WithGoclawSession.
// Returns "" if not set.
func goclawSessionFromCtx(ctx context.Context) string {
v, _ := ctx.Value(goclawSessionCtxKey).(string)
return v
}

// sensitiveEnvPrefixes lists env var prefixes stripped from ACP subprocesses.
var sensitiveEnvPrefixes = []string{
"GOCLAW", "CLAUDE", "ANTHROPIC", "OPENAI",
"DATABASE", "POSTGRES", "MYSQL", "REDIS", "MONGO",
"AWS_", "GOOGLE_", "AZURE_", "GCP_",
"AWS_", "AZURE_",
"GITHUB_", "GH_", "GITLAB_", "BITBUCKET_",
"DOCKER_", "REGISTRY_",
"STRIPE_", "TWILIO_", "SENDGRID_",
Expand Down
4 changes: 3 additions & 1 deletion internal/providers/acp/jsonrpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ func (c *Conn) readLoop() {

for scanner.Scan() {
line := scanner.Bytes()
slog.Debug("acp.jsonrpc: < READ", "line", string(line))
if len(line) == 0 {
continue
}
Expand Down Expand Up @@ -207,7 +208,8 @@ func (c *Conn) writeMessage(msg *jsonrpcMessage) error {

c.mu.Lock()
defer c.mu.Unlock()
_, err = c.writer.Write(data)
slog.Debug("acp.jsonrpc: > WRITE", "data", string(data))
_, err = c.writer.Write(data)
return err
}

Expand Down
Loading