@@ -12,6 +12,7 @@ package gemini
1212
1313import (
1414 "context"
15+ "encoding/json"
1516 "errors"
1617 "fmt"
1718 "os"
@@ -53,6 +54,12 @@ func (r *Runner) Run(ctx context.Context, req domain.RunRequest) (domain.RunResu
5354 return domain.RunResult {Role : req .Role , Err : err }, err
5455 }
5556
57+ credsPath , cleanup , err := materializeVertexCreds ()
58+ if err != nil {
59+ return domain.RunResult {Role : req .Role , Err : err }, err
60+ }
61+ defer cleanup ()
62+
5663 args := []string {"-p" , req .Prompt , "--output-format" , "text" , "-y" }
5764 model := req .Model
5865 if model == "" {
@@ -64,7 +71,7 @@ func (r *Runner) Run(ctx context.Context, req domain.RunRequest) (domain.RunResu
6471
6572 cmd := exec .CommandContext (ctx , r .bin (), args ... )
6673 cmd .Dir = req .WorkDir
67- cmd .Env = r .childEnv (req .WorkDir )
74+ cmd .Env = r .childEnv (req .WorkDir , credsPath )
6875 out , runErr := cmd .CombinedOutput ()
6976
7077 res := domain.RunResult {Role : req .Role , Summary : tail (strings .TrimSpace (string (out )), 40 )}
@@ -78,10 +85,11 @@ func (r *Runner) Run(ctx context.Context, req domain.RunRequest) (domain.RunResu
7885// childEnv builds the child environment. It mirrors the other adapters for the
7986// Go caches, and translates the AIXGO_VERTEX_* names into the names the Gemini
8087// CLI reads for Vertex.
81- func (r * Runner ) childEnv (workDir string ) []string {
88+ func (r * Runner ) childEnv (workDir , credsPath string ) []string {
8289 env := filterEnvKeys (os .Environ (),
8390 "GOCACHE" , "GOPATH" , "GH_TOKEN" , "GITHUB_TOKEN" ,
84- "GOOGLE_GENAI_USE_VERTEXAI" , "GOOGLE_CLOUD_PROJECT" , "GOOGLE_CLOUD_LOCATION" , "GOOGLE_API_KEY" ,
91+ "GOOGLE_GENAI_USE_VERTEXAI" , "GOOGLE_CLOUD_PROJECT" , "GOOGLE_CLOUD_LOCATION" ,
92+ "GOOGLE_API_KEY" , "GOOGLE_APPLICATION_CREDENTIALS" ,
8593 )
8694 env = append (env , r .ExtraEnv ... )
8795 if ! hasEnvKey (r .ExtraEnv , "GOCACHE" ) {
@@ -103,12 +111,51 @@ func (r *Runner) childEnv(workDir string) []string {
103111 loc = defaultLocation
104112 }
105113 env = append (env , "GOOGLE_CLOUD_LOCATION=" + loc )
106- if k := strings .TrimSpace (os .Getenv ("AIXGO_VERTEX_API_KEY" )); k != "" {
114+ if credsPath != "" {
115+ env = append (env , "GOOGLE_APPLICATION_CREDENTIALS=" + credsPath )
116+ } else if k := strings .TrimSpace (os .Getenv ("AIXGO_VERTEX_API_KEY" )); k != "" {
107117 env = append (env , "GOOGLE_API_KEY=" + k )
108118 }
109119 return env
110120}
111121
122+ // materializeVertexCreds writes a service-account JSON from AIXGO_VERTEX_API_KEY
123+ // to a temp file outside the worktree. A plain API key is left in the env and
124+ // handled by childEnv. The file is 0600 and must be removed by the caller.
125+ func materializeVertexCreds () (string , func (), error ) {
126+ noop := func () {}
127+ raw := strings .TrimSpace (os .Getenv ("AIXGO_VERTEX_API_KEY" ))
128+ if raw == "" || ! isServiceAccountJSON (raw ) {
129+ return "" , noop , nil
130+ }
131+ f , err := os .CreateTemp ("" , "aixgo-vertex-sa-*.json" )
132+ if err != nil {
133+ return "" , noop , fmt .Errorf ("gemini: write service account key: %w" , err )
134+ }
135+ path := f .Name ()
136+ cleanup := func () { _ = os .Remove (path ) }
137+ if _ , err := f .WriteString (raw ); err != nil {
138+ _ = f .Close ()
139+ cleanup ()
140+ return "" , noop , fmt .Errorf ("gemini: write service account key: %w" , err )
141+ }
142+ if err := f .Close (); err != nil {
143+ cleanup ()
144+ return "" , noop , fmt .Errorf ("gemini: write service account key: %w" , err )
145+ }
146+ return path , cleanup , nil
147+ }
148+
149+ func isServiceAccountJSON (s string ) bool {
150+ var doc struct {
151+ Type string `json:"type"`
152+ }
153+ if err := json .Unmarshal ([]byte (s ), & doc ); err != nil {
154+ return false
155+ }
156+ return doc .Type == "service_account"
157+ }
158+
112159func filterEnvKeys (env []string , keys ... string ) []string {
113160 out := make ([]string , 0 , len (env ))
114161 for _ , e := range env {
0 commit comments