mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 10:18:42 +00:00
* fix(cli): add missing X-GoClaw-User-Id header to gateway client * feat(acp): comprehensive integration with Gemini ACP protocol - Support nested JSON-RPC notification structures - Add robust streaming text collection and mapping - Increase handshake timeout to 60s for heavy model initialization - Fix WebSocket user_id authentication and schema v47 compatibility - Allow Google/GCP environment variables for ACP subprocesses * refactor(acp): multi-session architecture with session tracing and temp session cleanup - One shared Gemini process, multiple ACP sessions per process (one per goclaw conversation) - resolveSession: per-key mutex prevents TOCTOU race on concurrent session creation - Respawn detection via proc pointer comparison; session/load fallback after crash - sessionReaper: purges ACP sessions idle >30min; temp- sessions purged immediately on completion - WithGoclawSession context propagation: goclaw session key appears alongside ACP sid in all logs - dispatchUpdate: Gemini agent_message_chunk protocol mapping normalized here - session_test.go: full coverage of multi-session API (Initialize, NewSession, Prompt, Cancel, dispatch) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(acp): align with ACP protocol standard (SDK v0.16.1) - InitializeRequest: "capabilities" → "clientCapabilities" (standard field name) - LoadSessionRequest: add mcpServers field (required by standard) - mapStopReason: add standard stop reasons (max_tokens, cancelled) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(acp): address review findings from PR #901 - B1: Restore RequiredSchemaVersion to 55 (was incorrectly set to 47) - S1: Move X-GoClaw-User-Id header outside token check - S2: Add env gate (ACP_GEMINI_E2E) to Gemini E2E test - S3: Fix tab indentation in jsonrpc.go writeMessage - Build: Extract Pdeathsig to platform-specific files for cross-platform build * fix(acp): address S4-S7 review findings S4/S5: Send session/cancel before purging sessions locally - purgeSession() now cancels ACP session before deleting map entry - sessionReaper() sends cancel notification for idle sessions - Updated comments to reflect actual behavior S6: Tighten GOOGLE_/GCP_ env var filtering - Add GOOGLE_, GCP_ back to sensitiveEnvPrefixes - Add allowedEnvExact for safe vars: GOOGLE_API_KEY, GOOGLE_APPLICATION_CREDENTIALS, GOOGLE_CLOUD_PROJECT, GCP_PROJECT S7: Propagate ACP errors to callers - Chat/ChatStream now return err alongside ChatResponse - Enables upstream retry logic and error metrics --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: viettranx <viettranx@gmail.com>
115 lines
3.1 KiB
Go
115 lines
3.1 KiB
Go
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_", "AZURE_", "GOOGLE_", "GCP_",
|
|
"GITHUB_", "GH_", "GITLAB_", "BITBUCKET_",
|
|
"DOCKER_", "REGISTRY_",
|
|
"STRIPE_", "TWILIO_", "SENDGRID_",
|
|
"SSH_", "GPG_",
|
|
}
|
|
|
|
// allowedEnvExact lists env vars explicitly allowed even if they match sensitive prefixes.
|
|
// These are required for Google/GCP authentication in ACP subprocesses (e.g., Gemini).
|
|
var allowedEnvExact = map[string]bool{
|
|
"GOOGLE_API_KEY": true,
|
|
"GOOGLE_APPLICATION_CREDENTIALS": true,
|
|
"GOOGLE_CLOUD_PROJECT": true,
|
|
"GCP_PROJECT": true,
|
|
}
|
|
|
|
// sensitiveEnvExact lists exact env var names stripped from ACP subprocesses.
|
|
var sensitiveEnvExact = map[string]bool{
|
|
"DB_DSN": true, "PGPASSWORD": true, "PGUSER": true, "PGHOST": true,
|
|
"NPM_TOKEN": true, "NPM_CONFIG_TOKEN": true,
|
|
"HOMEBREW_GITHUB_API_TOKEN": true,
|
|
"CODECOV_TOKEN": true, "COVERALLS_REPO_TOKEN": true,
|
|
"SENTRY_DSN": true, "SENTRY_AUTH_TOKEN": true,
|
|
"SECRET_KEY": true, "JWT_SECRET": true,
|
|
}
|
|
|
|
// filterACPEnv strips sensitive env vars from the subprocess environment.
|
|
// Explicitly allowed vars (allowedEnvExact) pass through even if they match sensitive prefixes.
|
|
func filterACPEnv(environ []string) []string {
|
|
var filtered []string
|
|
for _, e := range environ {
|
|
key, _, _ := strings.Cut(e, "=")
|
|
upper := strings.ToUpper(key)
|
|
if allowedEnvExact[upper] {
|
|
filtered = append(filtered, e)
|
|
continue
|
|
}
|
|
if sensitiveEnvExact[upper] {
|
|
continue
|
|
}
|
|
skip := false
|
|
for _, prefix := range sensitiveEnvPrefixes {
|
|
if strings.HasPrefix(upper, prefix) {
|
|
skip = true
|
|
break
|
|
}
|
|
}
|
|
if skip {
|
|
continue
|
|
}
|
|
filtered = append(filtered, e)
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
// limitedWriter captures up to max bytes of output for diagnostics.
|
|
type limitedWriter struct {
|
|
mu sync.Mutex
|
|
buf []byte
|
|
max int
|
|
}
|
|
|
|
func (w *limitedWriter) Write(p []byte) (int, error) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
remaining := w.max - len(w.buf)
|
|
if remaining > 0 {
|
|
if len(p) > remaining {
|
|
p = p[:remaining]
|
|
}
|
|
w.buf = append(w.buf, p...)
|
|
}
|
|
return len(p), nil
|
|
}
|
|
|
|
func (w *limitedWriter) String() string {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
return string(w.buf)
|
|
}
|
|
|
|
// Ensure limitedWriter satisfies io.Writer.
|
|
var _ io.Writer = (*limitedWriter)(nil)
|