@@ -171,6 +171,12 @@ type Server struct {
171171
172172 executionRecorder ExecutionRecorder
173173 executionDispatcher ExecutionDispatcher
174+
175+ // resolvedSourceRoot is the agent's source tree, resolved once at
176+ // construction. A declaration edited afterwards can only make it stale, and
177+ // a stale root fails the prepared-mutation preview check loudly rather than
178+ // writing somewhere unverified.
179+ resolvedSourceRoot string
174180}
175181
176182// serviceExecution is the transport-independent behavior consumed by the
@@ -260,6 +266,7 @@ func NewServer(cfg Config) (*Server, error) {
260266 terminals : newTerminalManager (),
261267 executionRecorder : cfg .ExecutionRecorder ,
262268 executionDispatcher : cfg .ExecutionDispatcher ,
269+ resolvedSourceRoot : engine .AgentSourceRoot (context .Background (), cfg .WorkDir ),
263270 }
264271
265272 path := filepath .Join (cfg .WorkDir , "mind.yaml" )
@@ -510,11 +517,25 @@ func pluginToAgentName(plugin string) string {
510517 return plugin + ":latest"
511518}
512519
513- // serviceRoot returns the absolute path to the service source tree.
520+ // serviceRoot returns the absolute path to the service directory. It is the
521+ // basis for VCS, terminal and code-unit paths, which are addressed relative to
522+ // the checkout rather than to whatever subtree holds the source.
514523func (s * Server ) serviceRoot () string {
515524 return s .cfg .WorkDir
516525}
517526
527+ // sourceRoot returns the tree the service's agent resolves as its source root.
528+ // A service declaration moves it below the service directory with `source-dir`,
529+ // and the gateway now performs parser-derived work itself, so both sides must
530+ // agree on which tree they read and write. Disagreeing means previewing an edit
531+ // against one file and writing it to another of the same name.
532+ func (s * Server ) sourceRoot () string {
533+ if s .resolvedSourceRoot != "" {
534+ return s .resolvedSourceRoot
535+ }
536+ return s .cfg .WorkDir
537+ }
538+
518539// controlScope returns a control-plane handle scoped to this gateway's service,
519540// rooted at the service source tree. The Gateway is a thin adapter: generic
520541// operations (git today) delegate to the one control plane rather than
@@ -528,8 +549,10 @@ func (s *Server) controlScope() control.ServiceScope {
528549 return control .ServiceScopeAt (name , s .serviceRoot ())
529550}
530551
552+ // fileOps reads and writes project bytes, so it is rooted where the agent's
553+ // source lives rather than at the service directory.
531554func (s * Server ) fileOps () codecore.FileOperation {
532- return codecore .NewFileOps (codecore.LocalVFS {}, s .serviceRoot ())
555+ return codecore .NewFileOps (codecore.LocalVFS {}, s .sourceRoot ())
533556}
534557
535558func (s * Server ) validateService (service string ) error {
@@ -1169,7 +1192,7 @@ func (s *Server) ApplySymbolPatch(ctx context.Context, req *gatewayv1.ApplySymbo
11691192// needs a parser — DeclarationSpans and ValidateSyntax — which is exactly why
11701193// it belongs in the single binary that carries the analyzer.
11711194func (s * Server ) executeSymbolPatch (ctx context.Context , req * gatewayv1.ApplySymbolPatchRequest , rel string ) (* codev0.CodeResponse , error ) {
1172- return s .rootedSourceExecute (ctx , s .serviceRoot (), & codev0.CodeRequest {Operation : & codev0.CodeRequest_ApplySymbolPatch {ApplySymbolPatch : & codev0.ApplySymbolPatchRequest {
1195+ return s .rootedSourceExecute (ctx , s .sourceRoot (), & codev0.CodeRequest {Operation : & codev0.CodeRequest_ApplySymbolPatch {ApplySymbolPatch : & codev0.ApplySymbolPatchRequest {
11731196 File : rel , QualifiedName : req .GetQualifiedName (), ExpectedDeclarationSha256 : req .GetExpectedDeclarationSha256 (),
11741197 NewSource : req .GetNewSource (), FixMode : req .GetFixMode (), DryRun : req .GetDryRun (),
11751198 }}})
@@ -1195,7 +1218,7 @@ func gatewaySymbolPatchResponse(raw *codev0.CodeResponse) *gatewayv1.ApplySymbol
11951218
11961219func (s * Server ) applySymbolPatchWithReceipt (ctx context.Context , req * gatewayv1.ApplySymbolPatchRequest , rel , beforeSHA256 string , attempt * executionrecorder.Attempt ) (* gatewayv1.ApplySymbolPatchResponse , error ) {
11971220 effectStarted := time .Now ()
1198- raw , err := s .rootedSourceExecute (ctx , s .serviceRoot (), & codev0.CodeRequest {Operation : & codev0.CodeRequest_ApplySymbolPatch {ApplySymbolPatch : & codev0.ApplySymbolPatchRequest {
1221+ raw , err := s .rootedSourceExecute (ctx , s .sourceRoot (), & codev0.CodeRequest {Operation : & codev0.CodeRequest_ApplySymbolPatch {ApplySymbolPatch : & codev0.ApplySymbolPatchRequest {
11991222 File : rel , QualifiedName : req .GetQualifiedName (), ExpectedDeclarationSha256 : req .GetExpectedDeclarationSha256 (),
12001223 NewSource : req .GetNewSource (), FixMode : req .GetFixMode (), DryRun : false ,
12011224 }}})
@@ -1505,7 +1528,7 @@ func (s *Server) GetProjectInfo(ctx context.Context, req *gatewayv1.GetProjectIn
15051528 inventory := pi .GetSourceFiles ()
15061529 inventoryFailure := failures .Clone (resp .GetFailure ())
15071530 if pi .GetSourceFilesOmitted () {
1508- root := s .serviceRoot ()
1531+ root := s .sourceRoot ()
15091532 if inspected != nil {
15101533 root = inspected .root
15111534 }
@@ -1551,10 +1574,7 @@ func (s *Server) GetProjectInfo(ctx context.Context, req *gatewayv1.GetProjectIn
15511574// agent declined. An empty inventory without that flag is authoritative — the
15521575// unit genuinely has no source files — so only a declined one is refilled here.
15531576func (s * Server ) takeSourceImports (ctx context.Context , root , language string ) ([]* codev0.SourceFileInfo , error ) {
1554- if s .host == nil {
1555- return nil , fmt .Errorf ("workspace source behavior is unavailable" )
1556- }
1557- source , err := s .host .SourceAt (root )
1577+ source , err := s .boundSource (root )
15581578 if err != nil {
15591579 return nil , err
15601580 }
@@ -1566,6 +1586,11 @@ func (s *Server) takeSourceImports(ctx context.Context, root, language string) (
15661586// the engine, which carries the source-semantics analyzer. Routing it to the
15671587// owning agent instead would make every language agent link the tree-sitter
15681588// CGO stack to answer an operation none of them implements.
1589+ //
1590+ // A source_agents entry for the unit therefore selects nothing here: the
1591+ // projection is language-neutral and the analyzer is the same one every agent
1592+ // would have installed. The entry still selects the agent for operations an
1593+ // agent actually owns.
15691594func (s * Server ) GetSemanticIndex (ctx context.Context , req * gatewayv1.GetSemanticIndexRequest ) (* gatewayv1.GetSemanticIndexResponse , error ) {
15701595 requestedService := ""
15711596 if req != nil {
@@ -1606,17 +1631,23 @@ func (s *Server) GetSemanticIndex(ctx context.Context, req *gatewayv1.GetSemanti
16061631// exact root, so a parser-derived answer describes that tree and no other. The
16071632// host-wide behavior of sourceExecute can sit above the root in question.
16081633func (s * Server ) rootedSourceExecute (ctx context.Context , root string , request * codev0.CodeRequest ) (* codev0.CodeResponse , error ) {
1609- if s .host == nil {
1610- return nil , fmt .Errorf ("workspace source behavior is unavailable" )
1611- }
1612- source , err := s .host .SourceAt (root )
1634+ source , err := s .boundSource (root )
16131635 if err != nil {
16141636 return nil , err
16151637 }
16161638 defer func () { _ = source .Close () }()
16171639 return source .ExecuteCode (ctx , request )
16181640}
16191641
1642+ // boundSource binds engine source behavior to an exact root. The caller owns
1643+ // the result and closes it.
1644+ func (s * Server ) boundSource (root string ) (* engine.Source , error ) {
1645+ if s .host == nil {
1646+ return nil , fmt .Errorf ("workspace source behavior is unavailable" )
1647+ }
1648+ return s .host .SourceAt (root )
1649+ }
1650+
16201651// GetSourceManifest invokes the language-neutral rooted Code behavior so every
16211652// workspace has the same body-free artifact inventory regardless of which
16221653// language agents are installed. The Gateway forwards typed identities only;
0 commit comments