Skip to content

Commit 6c3317a

Browse files
authored
feat(remotesync): prune forbidden roots nested inside allowed archive roots (#1283)
Builds on #1282, which added the `RemoteSyncExcluded` capability. Remote sync already skips excluded agents (currently Trae), but an excluded root nested inside another agent's allowed root — possible with overlapping directory overrides — would still end up in archives and tarballs. Fix: `TargetSet` carries `ForbiddenRoots`, always re-derived server-side, and every layer enforces them — target resolution omits nested targets, request validation rejects them, the archive/manifest walkers prune them, and the SSH path excludes them from the generated tar command. All checks go through one predicate, `remotesync.PathWithinForbiddenRoots`. Paths are canonicalized before comparison (absolute, symlinks resolved, case-folded on Windows/macOS; the SSH script emits physical paths), so an alias spelling of a forbidden root cannot slip past. Resolver output parsing fails the sync rather than ever dropping an exclusion boundary. Notes: - With an env override (e.g. `TRAE_DIR`), the override is the protected root; default locations are not additionally protected — same as allowed-agent resolution. - Tar exclude patterns are glob-escaped and covered by a test against real tar. Where to look: `internal/remotesync/paths.go` (predicate + canonicalization), `resolve.go` (advertised-set filtering), `internal/ssh/resolve.go` (script + parsing), `internal/ssh/transfer.go` (tar exclusion). Co-authored-by: Wes McKinney <wesm@users.noreply.github.com>
1 parent 3bf7248 commit 6c3317a

14 files changed

Lines changed: 1847 additions & 141 deletions

internal/remotesync/archive.go

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414

1515
func WriteArchive(w io.Writer, targets TargetSet) error {
1616
tw := tar.NewWriter(w)
17+
forbidden := newForbiddenRootMatcher(targets.ForbiddenRoots)
1718
hermesSQLite := make(map[string]string)
1819
for _, stateDB := range hermesStateDBTargets(targets) {
1920
for _, path := range hermesSQLitePaths(stateDB) {
@@ -22,6 +23,9 @@ func WriteArchive(w io.Writer, targets TargetSet) error {
2223
}
2324
writtenHermesState := make(map[string]struct{})
2425
writePath := func(path string, optional bool) error {
26+
if forbidden.within(path) {
27+
return nil
28+
}
2529
clean := filepath.Clean(path)
2630
if stateDB, ok := hermesSQLite[clean]; ok {
2731
if _, written := writtenHermesState[stateDB]; written {
@@ -33,9 +37,12 @@ func WriteArchive(w io.Writer, targets TargetSet) error {
3337
if optional {
3438
return writeOptionalArchiveFile(tw, path)
3539
}
36-
return writeArchivePath(tw, path)
40+
return writeArchivePath(tw, path, forbidden)
3741
}
3842
for agent, dirs := range targets.Dirs {
43+
if parser.RemoteSyncExcludedAgent(agent) {
44+
continue
45+
}
3946
if _, fileScoped := targets.Files[agent]; fileScoped {
4047
continue
4148
}
@@ -46,8 +53,11 @@ func WriteArchive(w io.Writer, targets TargetSet) error {
4653
}
4754
}
4855
for agent, files := range targets.Files {
56+
if parser.RemoteSyncExcludedAgent(agent) {
57+
continue
58+
}
4959
if agent == parser.AgentWindsurf {
50-
if err := writeWindsurfArchiveFiles(tw, files); err != nil {
60+
if err := writeWindsurfArchiveFiles(tw, files, forbidden); err != nil {
5161
return err
5262
}
5363
continue
@@ -99,9 +109,14 @@ func writeHermesStateDBSnapshot(tw *tar.Writer, stateDB string) error {
99109

100110
var writeHermesSnapshotFile = writeSQLiteSnapshot
101111

102-
func writeWindsurfArchiveFiles(tw *tar.Writer, files []string) error {
112+
func writeWindsurfArchiveFiles(
113+
tw *tar.Writer, files []string, forbidden forbiddenRootMatcher,
114+
) error {
103115
seen := make(map[string]struct{}, len(files))
104116
for _, path := range files {
117+
if forbidden.within(path) {
118+
continue
119+
}
105120
if _, ok := seen[path]; ok {
106121
continue
107122
}
@@ -187,7 +202,12 @@ func writeOptionalArchiveFile(tw *tar.Writer, path string) error {
187202
return writeArchiveFile(tw, path, info)
188203
}
189204

190-
func writeArchivePath(tw *tar.Writer, root string) error {
205+
func writeArchivePath(
206+
tw *tar.Writer, root string, forbidden forbiddenRootMatcher,
207+
) error {
208+
if forbidden.within(root) {
209+
return nil
210+
}
191211
info, err := os.Lstat(root)
192212
if err != nil {
193213
return fmt.Errorf("stat archive path %q: %w", root, err)
@@ -205,6 +225,12 @@ func writeArchivePath(tw *tar.Writer, root string) error {
205225
}
206226
return err
207227
}
228+
if forbidden.within(path) {
229+
if entry.IsDir() {
230+
return filepath.SkipDir
231+
}
232+
return nil
233+
}
208234
info, err := entry.Info()
209235
if err != nil {
210236
if os.IsNotExist(err) {
@@ -337,14 +363,15 @@ func writeArchiveHeader(
337363
// validate.
338364
func WriteArchiveFiles(w io.Writer, allowed TargetSet, files []string) error {
339365
tw := tar.NewWriter(w)
366+
forbidden := newForbiddenRootMatcher(allowed.ForbiddenRoots)
340367
allowedRoots := allowed.DeltaAllowedRoots()
341368
hermesStateDBs := make(map[string]struct{})
342369
for _, stateDB := range hermesStateDBTargets(allowed) {
343370
hermesStateDBs[filepath.Clean(stateDB)] = struct{}{}
344371
}
345372
writtenHermesState := make(map[string]struct{})
346373
for _, path := range files {
347-
local, ok := resolveDeltaFilePath(allowedRoots, path)
374+
local, ok := resolveDeltaFilePath(allowedRoots, forbidden, path)
348375
if !ok {
349376
continue
350377
}
@@ -386,18 +413,21 @@ func WriteArchiveFiles(w io.Writer, allowed TargetSet, files []string) error {
386413
// root returns filepath.Join(root, rel) where rel passed
387414
// filepath.IsLocal, so the path used for filesystem access is always
388415
// derived from a trusted base rather than the request string.
389-
func resolveDeltaFilePath(allowedRoots []string, path string) (string, bool) {
416+
func resolveDeltaFilePath(
417+
allowedRoots []string, forbidden forbiddenRootMatcher, path string,
418+
) (string, bool) {
390419
clean := filepath.Clean(path)
391420
for _, root := range allowedRoots {
392421
root = filepath.Clean(root)
393422
if clean == root {
394-
return root, true
423+
return root, !forbidden.within(root)
395424
}
396425
rel, err := filepath.Rel(root, clean)
397426
if err != nil || !filepath.IsLocal(rel) {
398427
continue
399428
}
400-
return filepath.Join(root, rel), true
429+
local := filepath.Join(root, rel)
430+
return local, !forbidden.within(local)
401431
}
402432
return "", false
403433
}

internal/remotesync/archive_test.go

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,101 @@ func TestHermesArchivesSnapshotWALCommitBeforeCheckpoint(t *testing.T) {
240240
}
241241
}
242242

243+
func TestWriteArchiveExcludesRemoteSyncExcludedAgentState(t *testing.T) {
244+
root := t.TempDir()
245+
chatDB := filepath.Join(root, "chat.db")
246+
require.NoError(t, os.WriteFile(chatDB, []byte("authentication state"), 0o600))
247+
require.NoError(t, os.WriteFile(
248+
filepath.Join(root, "credentials.json"), []byte("secret"), 0o600,
249+
))
250+
251+
targets := TargetSet{
252+
Dirs: map[parser.AgentType][]string{
253+
parser.AgentTrae: {root},
254+
},
255+
Files: map[parser.AgentType][]string{
256+
parser.AgentTrae: {chatDB},
257+
},
258+
}
259+
for _, tt := range []struct {
260+
name string
261+
write func(io.Writer) error
262+
}{
263+
{
264+
name: "full",
265+
write: func(w io.Writer) error {
266+
return WriteArchive(w, targets)
267+
},
268+
},
269+
{
270+
name: "delta",
271+
write: func(w io.Writer) error {
272+
return WriteArchiveFiles(w, targets, []string{chatDB})
273+
},
274+
},
275+
} {
276+
t.Run(tt.name, func(t *testing.T) {
277+
var archive bytes.Buffer
278+
require.NoError(t, tt.write(&archive))
279+
_, err := tar.NewReader(&archive).Next()
280+
assert.ErrorIs(t, err, io.EOF,
281+
"a remote-sync-excluded agent's state must never enter a remote archive")
282+
})
283+
}
284+
}
285+
286+
func TestWriteArchivePrunesForbiddenRootNestedInAllowedRoot(t *testing.T) {
287+
root := t.TempDir()
288+
allowed := filepath.Join(root, "sessions")
289+
forbidden := filepath.Join(allowed, ".forbidden-provider")
290+
keep := filepath.Join(allowed, "session.jsonl")
291+
secret := filepath.Join(forbidden, "chat.db")
292+
require.NoError(t, os.MkdirAll(forbidden, 0o755))
293+
require.NoError(t, os.WriteFile(keep, []byte("session"), 0o644))
294+
require.NoError(t, os.WriteFile(secret, []byte("authentication state"), 0o600))
295+
296+
targets := TargetSet{
297+
Dirs: map[parser.AgentType][]string{parser.AgentClaude: {allowed}},
298+
ForbiddenRoots: []string{forbidden},
299+
}
300+
for _, tt := range []struct {
301+
name string
302+
write func(io.Writer) error
303+
}{
304+
{
305+
name: "full archive",
306+
write: func(w io.Writer) error {
307+
return WriteArchive(w, targets)
308+
},
309+
},
310+
{
311+
name: "delta archive",
312+
write: func(w io.Writer) error {
313+
return WriteArchiveFiles(w, targets, []string{keep, secret})
314+
},
315+
},
316+
} {
317+
t.Run(tt.name, func(t *testing.T) {
318+
var archive bytes.Buffer
319+
require.NoError(t, tt.write(&archive))
320+
321+
var names []string
322+
tr := tar.NewReader(&archive)
323+
for {
324+
hdr, err := tr.Next()
325+
if errors.Is(err, io.EOF) {
326+
break
327+
}
328+
require.NoError(t, err)
329+
names = append(names, hdr.Name)
330+
}
331+
assert.Contains(t, names, archiveNameForTest(t, keep))
332+
assert.NotContains(t, names, archiveNameForTest(t, secret),
333+
"a forbidden nested root must not enter the transfer artifact")
334+
})
335+
}
336+
}
337+
243338
func TestWriteArchivePropagatesAdvertisedHermesSnapshotFailure(t *testing.T) {
244339
stateDB := filepath.Join(t.TempDir(), "profile", "state.db")
245340
require.NoError(t, os.MkdirAll(filepath.Dir(stateDB), 0o755))
@@ -563,7 +658,7 @@ func TestResolveDeltaFilePath(t *testing.T) {
563658
for _, tt := range tests {
564659
t.Run(tt.name, func(t *testing.T) {
565660
got, ok := resolveDeltaFilePath(
566-
fromSlashAll(tt.roots), filepath.FromSlash(tt.path))
661+
fromSlashAll(tt.roots), forbiddenRootMatcher{}, filepath.FromSlash(tt.path))
567662
require.Equal(t, tt.ok, ok)
568663
assert.Equal(t, filepath.FromSlash(tt.want), got)
569664
})

internal/remotesync/manifest.go

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"os"
77
"path/filepath"
88
"sort"
9+
10+
"go.kenn.io/agentsview/internal/parser"
911
)
1012

1113
// ManifestEntry describes one regular file available for remote sync.
@@ -41,6 +43,7 @@ func BuildManifest(targets TargetSet) (Manifest, error) {
4143
"manifest not supported for sanitized file-scoped agents")
4244
}
4345
m := Manifest{Files: []ManifestEntry{}}
46+
forbidden := newForbiddenRootMatcher(targets.ForbiddenRoots)
4447
hermesStateDBs := hermesStateDBTargets(targets)
4548
hermesSQLite := make(map[string]struct{}, len(hermesStateDBs)*4)
4649
for _, stateDB := range hermesStateDBs {
@@ -56,6 +59,9 @@ func BuildManifest(targets TargetSet) (Manifest, error) {
5659
})
5760
}
5861
addLstat := func(path string) error {
62+
if forbidden.within(path) {
63+
return nil
64+
}
5965
info, err := os.Lstat(path)
6066
if err != nil {
6167
if os.IsNotExist(err) {
@@ -69,19 +75,25 @@ func BuildManifest(targets TargetSet) (Manifest, error) {
6975
return nil
7076
}
7177
for agent, dirs := range targets.Dirs {
78+
if parser.RemoteSyncExcludedAgent(agent) {
79+
continue
80+
}
7281
if _, fileScoped := targets.Files[agent]; fileScoped {
7382
continue
7483
}
7584
for _, root := range dirs {
7685
if _, ok := hermesSQLite[filepath.Clean(root)]; ok {
7786
continue
7887
}
79-
if err := manifestWalk(root, add); err != nil {
88+
if err := manifestWalk(root, forbidden, add); err != nil {
8089
return Manifest{}, err
8190
}
8291
}
8392
}
84-
for _, files := range targets.Files {
93+
for agent, files := range targets.Files {
94+
if parser.RemoteSyncExcludedAgent(agent) {
95+
continue
96+
}
8597
for _, path := range files {
8698
if _, ok := hermesSQLite[filepath.Clean(path)]; ok {
8799
continue
@@ -100,6 +112,9 @@ func BuildManifest(targets TargetSet) (Manifest, error) {
100112
}
101113
}
102114
for _, stateDB := range hermesStateDBs {
115+
if forbidden.within(stateDB) {
116+
continue
117+
}
103118
size, modTime, exists := hermesSQLiteSnapshotIdentity(stateDB)
104119
if exists {
105120
m.Files = append(m.Files, ManifestEntry{
@@ -113,7 +128,12 @@ func BuildManifest(targets TargetSet) (Manifest, error) {
113128
return m, nil
114129
}
115130

116-
func manifestWalk(root string, add func(string, os.FileInfo)) error {
131+
func manifestWalk(
132+
root string, forbidden forbiddenRootMatcher, add func(string, os.FileInfo),
133+
) error {
134+
if forbidden.within(root) {
135+
return nil
136+
}
117137
info, err := os.Lstat(root)
118138
if err != nil {
119139
if os.IsNotExist(err) {
@@ -137,6 +157,12 @@ func manifestWalk(root string, add func(string, os.FileInfo)) error {
137157
}
138158
return err
139159
}
160+
if forbidden.within(path) {
161+
if entry.IsDir() {
162+
return filepath.SkipDir
163+
}
164+
return nil
165+
}
140166
info, err := entry.Info()
141167
if err != nil {
142168
if os.IsNotExist(err) {

internal/remotesync/manifest_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,28 @@ func TestBuildManifestToleratesMissingRootsAndExtraFiles(t *testing.T) {
5757
assert.Empty(t, m.Files)
5858
}
5959

60+
func TestBuildManifestPrunesForbiddenRootNestedInAllowedRoot(t *testing.T) {
61+
root := t.TempDir()
62+
allowed := filepath.Join(root, "sessions")
63+
forbidden := filepath.Join(allowed, ".forbidden-provider")
64+
keep := filepath.Join(allowed, "session.jsonl")
65+
secret := filepath.Join(forbidden, "chat.db")
66+
require.NoError(t, os.MkdirAll(forbidden, 0o755))
67+
require.NoError(t, os.WriteFile(keep, []byte("session"), 0o644))
68+
require.NoError(t, os.WriteFile(secret, []byte("authentication state"), 0o600))
69+
70+
manifest, err := BuildManifest(TargetSet{
71+
Dirs: map[parser.AgentType][]string{parser.AgentClaude: {allowed}},
72+
ForbiddenRoots: []string{forbidden},
73+
})
74+
75+
require.NoError(t, err)
76+
require.Len(t, manifest.Files, 1)
77+
assert.Equal(t, keep, manifest.Files[0].Path)
78+
assert.NotEqual(t, secret, manifest.Files[0].Path,
79+
"the manifest must never advertise a file under a forbidden root")
80+
}
81+
6082
func TestBuildManifestRejectsFileScopedAgents(t *testing.T) {
6183
_, err := BuildManifest(TargetSet{
6284
Dirs: map[parser.AgentType][]string{parser.AgentWindsurf: {"/srv/Windsurf/User"}},

0 commit comments

Comments
 (0)