@@ -14,6 +14,7 @@ import (
1414 "github.com/stacklok/apiary/internal/domain/agent"
1515 "github.com/stacklok/apiary/internal/domain/config"
1616 "github.com/stacklok/apiary/internal/domain/egress"
17+ domaingit "github.com/stacklok/apiary/internal/domain/git"
1718 "github.com/stacklok/apiary/internal/domain/hostservice"
1819 "github.com/stacklok/apiary/internal/domain/progress"
1920 "github.com/stacklok/apiary/internal/domain/session"
@@ -62,6 +63,12 @@ type RunOpts struct {
6263 // Snapshot holds snapshot isolation options.
6364 Snapshot SnapshotOpts
6465
66+ // GitTokenEnabled controls whether git token env vars are forwarded.
67+ GitTokenEnabled bool
68+
69+ // SSHAgentForward enables SSH agent forwarding to the VM.
70+ SSHAgentForward bool
71+
6572 // Terminal provides I/O streams for the session. Required for Run().
6673 Terminal session.Terminal
6774}
@@ -84,18 +91,25 @@ type SandboxDeps struct {
8491
8592 // MCPProvider creates host services for MCP proxy (nil = disabled).
8693 MCPProvider hostservice.Provider
94+
95+ // SnapshotPostProcessors run after snapshot creation but before VM start.
96+ SnapshotPostProcessors []workspace.SnapshotPostProcessor
97+
98+ // GitIdentityProvider resolves the host git user identity.
99+ GitIdentityProvider domaingit.IdentityProvider
87100}
88101
89102// Sandbox holds the state of a running sandbox session.
90103// Created by Prepare, consumed by Attach/Stop/Changes/Flush/Cleanup.
91104type Sandbox struct {
92- Agent agent.Agent
93- VM domvm.VM
94- VMConfig domvm.VMConfig
95- Snapshot * workspace.Snapshot
96- WorkspacePath string
97- DiffMatcher snapshot.Matcher
98- EnvVars map [string ]string
105+ Agent agent.Agent
106+ VM domvm.VM
107+ VMConfig domvm.VMConfig
108+ Snapshot * workspace.Snapshot
109+ WorkspacePath string
110+ DiffMatcher snapshot.Matcher
111+ EnvVars map [string ]string
112+ SSHAgentForward bool
99113}
100114
101115// Cleanup releases resources (snapshot dir). Safe to call multiple times.
@@ -116,18 +130,20 @@ func (sb *Sandbox) Cleanup() error {
116130// Changes(), Flush(), and Sandbox.Cleanup() individually. This allows the caller
117131// to control terminal attachment, async review workflows, and concurrent sessions.
118132type SandboxRunner struct {
119- registry agent.Registry
120- vmRunner domvm.VMRunner
121- sessionRunner session.TerminalSession
122- config * config.Config
123- envProvider agent.EnvProvider
124- logger * slog.Logger
125- observer progress.Observer
126- workspaceCloner workspace.WorkspaceCloner
127- reviewer snapshot.Reviewer
128- flusher snapshot.Flusher
129- differ snapshot.Differ
130- mcpProvider hostservice.Provider
133+ registry agent.Registry
134+ vmRunner domvm.VMRunner
135+ sessionRunner session.TerminalSession
136+ config * config.Config
137+ envProvider agent.EnvProvider
138+ logger * slog.Logger
139+ observer progress.Observer
140+ workspaceCloner workspace.WorkspaceCloner
141+ reviewer snapshot.Reviewer
142+ flusher snapshot.Flusher
143+ differ snapshot.Differ
144+ mcpProvider hostservice.Provider
145+ snapshotPostProcessors []workspace.SnapshotPostProcessor
146+ gitIdentityProvider domaingit.IdentityProvider
131147}
132148
133149// NewSandboxRunner creates a new SandboxRunner with the given dependencies.
@@ -137,18 +153,20 @@ func NewSandboxRunner(deps SandboxDeps) *SandboxRunner {
137153 obs = progress .Nop ()
138154 }
139155 return & SandboxRunner {
140- registry : deps .Registry ,
141- vmRunner : deps .VMRunner ,
142- sessionRunner : deps .SessionRunner ,
143- config : deps .Config ,
144- envProvider : deps .EnvProvider ,
145- logger : deps .Logger ,
146- observer : obs ,
147- workspaceCloner : deps .WorkspaceCloner ,
148- reviewer : deps .Reviewer ,
149- flusher : deps .Flusher ,
150- differ : deps .Differ ,
151- mcpProvider : deps .MCPProvider ,
156+ registry : deps .Registry ,
157+ vmRunner : deps .VMRunner ,
158+ sessionRunner : deps .SessionRunner ,
159+ config : deps .Config ,
160+ envProvider : deps .EnvProvider ,
161+ logger : deps .Logger ,
162+ observer : obs ,
163+ workspaceCloner : deps .WorkspaceCloner ,
164+ reviewer : deps .Reviewer ,
165+ flusher : deps .Flusher ,
166+ differ : deps .Differ ,
167+ mcpProvider : deps .MCPProvider ,
168+ snapshotPostProcessors : deps .SnapshotPostProcessors ,
169+ gitIdentityProvider : deps .GitIdentityProvider ,
152170 }
153171}
154172
@@ -230,8 +248,12 @@ func (s *SandboxRunner) Prepare(ctx context.Context, agentName string, opts RunO
230248 )
231249 }
232250
233- // 3. Collect env vars.
234- envVars := agent .ForwardEnv (ag .EnvForward , s .envProvider )
251+ // 3. Collect env vars (merge common git patterns when token forwarding is enabled).
252+ allPatterns := ag .EnvForward
253+ if opts .GitTokenEnabled {
254+ allPatterns = mergeEnvPatterns (allPatterns , domaingit .CommonEnvPatterns ())
255+ }
256+ envVars := agent .ForwardEnv (allPatterns , s .envProvider )
235257 if len (envVars ) > 0 {
236258 keys := make ([]string , 0 , len (envVars ))
237259 for k := range envVars {
@@ -240,6 +262,44 @@ func (s *SandboxRunner) Prepare(ctx context.Context, agentName string, opts RunO
240262 s .logger .Debug ("forwarding environment variables" , "keys" , keys )
241263 }
242264
265+ // Resolve git identity (fallback for env vars not already set).
266+ var gitIdentity domaingit.Identity
267+ if s .gitIdentityProvider != nil {
268+ id , idErr := s .gitIdentityProvider .GetIdentity ()
269+ if idErr != nil {
270+ s .logger .Warn ("failed to resolve git identity" , "error" , idErr )
271+ } else {
272+ gitIdentity = id
273+ }
274+ }
275+
276+ // Inject git identity into env vars as fallback when not already present.
277+ if gitIdentity .Name != "" {
278+ if envVars == nil {
279+ envVars = make (map [string ]string )
280+ }
281+ if _ , ok := envVars ["GIT_AUTHOR_NAME" ]; ! ok {
282+ envVars ["GIT_AUTHOR_NAME" ] = gitIdentity .Name
283+ }
284+ if _ , ok := envVars ["GIT_COMMITTER_NAME" ]; ! ok {
285+ envVars ["GIT_COMMITTER_NAME" ] = gitIdentity .Name
286+ }
287+ }
288+ if gitIdentity .Email != "" {
289+ if envVars == nil {
290+ envVars = make (map [string ]string )
291+ }
292+ if _ , ok := envVars ["GIT_AUTHOR_EMAIL" ]; ! ok {
293+ envVars ["GIT_AUTHOR_EMAIL" ] = gitIdentity .Email
294+ }
295+ if _ , ok := envVars ["GIT_COMMITTER_EMAIL" ]; ! ok {
296+ envVars ["GIT_COMMITTER_EMAIL" ] = gitIdentity .Email
297+ }
298+ }
299+
300+ // Determine if a GitHub token is available for credential helper injection.
301+ hasGitToken := opts .GitTokenEnabled && (envVars ["GITHUB_TOKEN" ] != "" || envVars ["GH_TOKEN" ] != "" )
302+
243303 // 4. Set up MCP host services if enabled.
244304 var hostServices []domvm.HostService
245305 mcpCfg := s .resolveMCPConfig (cfg , agentName )
@@ -286,6 +346,20 @@ func (s *SandboxRunner) Prepare(ctx context.Context, agentName string, opts RunO
286346 "original" , snap .OriginalPath ,
287347 "snapshot" , snap .SnapshotPath ,
288348 )
349+
350+ // Run post-processors on the snapshot (e.g., git config sanitizer).
351+ // Failures abort VM start — post-processors are security-relevant
352+ // (credential stripping) and must not be silently skipped.
353+ for _ , pp := range s .snapshotPostProcessors {
354+ if ppErr := pp .Process (ctx , snap .OriginalPath , snap .SnapshotPath ); ppErr != nil {
355+ s .observer .Fail ("Snapshot post-processing failed" )
356+ if cleanErr := snap .Cleanup (); cleanErr != nil {
357+ s .logger .Error ("failed to clean up snapshot after post-processor failure" , "error" , cleanErr )
358+ }
359+ return nil , fmt .Errorf ("snapshot post-processing: %w" , ppErr )
360+ }
361+ }
362+
289363 workspacePath = snap .SnapshotPath
290364 }
291365
@@ -303,6 +377,9 @@ func (s *SandboxRunner) Prepare(ctx context.Context, agentName string, opts RunO
303377 EgressPolicy : egressPolicy ,
304378 HostServices : hostServices ,
305379 MCPConfigFormat : ag .MCPConfigFormat ,
380+ GitIdentity : gitIdentity ,
381+ HasGitToken : hasGitToken ,
382+ SSHAgentForward : opts .SSHAgentForward ,
306383 }
307384
308385 sandboxVM , err := s .vmRunner .Start (ctx , vmCfg )
@@ -320,13 +397,14 @@ func (s *SandboxRunner) Prepare(ctx context.Context, agentName string, opts RunO
320397 s .observer .Complete ("Sandbox ready" )
321398
322399 return & Sandbox {
323- Agent : ag ,
324- VM : sandboxVM ,
325- VMConfig : vmCfg ,
326- Snapshot : snap ,
327- WorkspacePath : workspacePath ,
328- DiffMatcher : diffMatcher ,
329- EnvVars : envVars ,
400+ Agent : ag ,
401+ VM : sandboxVM ,
402+ VMConfig : vmCfg ,
403+ Snapshot : snap ,
404+ WorkspacePath : workspacePath ,
405+ DiffMatcher : diffMatcher ,
406+ EnvVars : envVars ,
407+ SSHAgentForward : opts .SSHAgentForward ,
330408 }, nil
331409}
332410
@@ -335,12 +413,13 @@ func (s *SandboxRunner) Prepare(ctx context.Context, agentName string, opts RunO
335413// The terminal parameter provides I/O streams and PTY control for this session.
336414func (s * SandboxRunner ) Attach (ctx context.Context , sb * Sandbox , terminal session.Terminal ) error {
337415 sessionOpts := session.SessionOpts {
338- Host : "127.0.0.1" ,
339- Port : sb .VM .SSHPort (),
340- User : "sandbox" ,
341- KeyPath : sb .VM .SSHKeyPath (),
342- Command : sb .Agent .Command ,
343- Terminal : terminal ,
416+ Host : "127.0.0.1" ,
417+ Port : sb .VM .SSHPort (),
418+ User : "sandbox" ,
419+ KeyPath : sb .VM .SSHKeyPath (),
420+ Command : sb .Agent .Command ,
421+ Terminal : terminal ,
422+ SSHAgentForward : sb .SSHAgentForward ,
344423 }
345424
346425 s .logger .Debug ("connecting to sandbox VM" ,
@@ -449,6 +528,23 @@ func (s *SandboxRunner) Run(ctx context.Context, agentName string, opts RunOpts)
449528 return reviewErr
450529}
451530
531+ // mergeEnvPatterns combines two pattern lists, deduplicating entries.
532+ func mergeEnvPatterns (base , extra []string ) []string {
533+ seen := make (map [string ]bool , len (base ))
534+ for _ , p := range base {
535+ seen [p ] = true
536+ }
537+ merged := make ([]string , len (base ))
538+ copy (merged , base )
539+ for _ , p := range extra {
540+ if ! seen [p ] {
541+ merged = append (merged , p )
542+ seen [p ] = true
543+ }
544+ }
545+ return merged
546+ }
547+
452548// resolveMCPConfig returns the effective MCP configuration by merging
453549// global config with any agent-specific override.
454550func (s * SandboxRunner ) resolveMCPConfig (cfg * config.Config , agentName string ) config.MCPConfig {
0 commit comments