feat: deny-by-default filesystem isolation
- Deny-by-default filesystem isolation for Linux (Landlock) and macOS (Seatbelt) - Prevent learning mode from collapsing read paths to $HOME - Add Linux deny-by-default lessons to experience docs
This commit is contained in:
@@ -36,7 +36,7 @@ type NetworkConfig struct {
|
||||
|
||||
// FilesystemConfig defines filesystem restrictions.
|
||||
type FilesystemConfig struct {
|
||||
DefaultDenyRead bool `json:"defaultDenyRead,omitempty"` // If true, deny reads by default except system paths and AllowRead
|
||||
DefaultDenyRead *bool `json:"defaultDenyRead,omitempty"` // If nil or true, deny reads by default except system paths, CWD, and AllowRead
|
||||
AllowRead []string `json:"allowRead"` // Paths to allow reading (used when DefaultDenyRead is true)
|
||||
DenyRead []string `json:"denyRead"`
|
||||
AllowWrite []string `json:"allowWrite"`
|
||||
@@ -44,6 +44,12 @@ type FilesystemConfig struct {
|
||||
AllowGitConfig bool `json:"allowGitConfig,omitempty"`
|
||||
}
|
||||
|
||||
// IsDefaultDenyRead returns whether deny-by-default read mode is enabled.
|
||||
// Defaults to true when not explicitly set (nil).
|
||||
func (f *FilesystemConfig) IsDefaultDenyRead() bool {
|
||||
return f.DefaultDenyRead == nil || *f.DefaultDenyRead
|
||||
}
|
||||
|
||||
// CommandConfig defines command restrictions.
|
||||
type CommandConfig struct {
|
||||
Deny []string `json:"deny"`
|
||||
@@ -417,8 +423,8 @@ func Merge(base, override *Config) *Config {
|
||||
},
|
||||
|
||||
Filesystem: FilesystemConfig{
|
||||
// Boolean fields: true if either enables it
|
||||
DefaultDenyRead: base.Filesystem.DefaultDenyRead || override.Filesystem.DefaultDenyRead,
|
||||
// Pointer field: override wins if set, otherwise base (nil = deny-by-default)
|
||||
DefaultDenyRead: mergeOptionalBool(base.Filesystem.DefaultDenyRead, override.Filesystem.DefaultDenyRead),
|
||||
|
||||
// Append slices
|
||||
AllowRead: mergeStrings(base.Filesystem.AllowRead, override.Filesystem.AllowRead),
|
||||
|
||||
@@ -410,7 +410,7 @@ func TestMerge(t *testing.T) {
|
||||
t.Run("merge defaultDenyRead and allowRead", func(t *testing.T) {
|
||||
base := &Config{
|
||||
Filesystem: FilesystemConfig{
|
||||
DefaultDenyRead: true,
|
||||
DefaultDenyRead: boolPtr(true),
|
||||
AllowRead: []string{"/home/user/project"},
|
||||
},
|
||||
}
|
||||
@@ -421,13 +421,40 @@ func TestMerge(t *testing.T) {
|
||||
}
|
||||
result := Merge(base, override)
|
||||
|
||||
if !result.Filesystem.DefaultDenyRead {
|
||||
t.Error("expected DefaultDenyRead to be true (from base)")
|
||||
if !result.Filesystem.IsDefaultDenyRead() {
|
||||
t.Error("expected IsDefaultDenyRead() to be true (from base)")
|
||||
}
|
||||
if len(result.Filesystem.AllowRead) != 2 {
|
||||
t.Errorf("expected 2 allowRead paths, got %d: %v", len(result.Filesystem.AllowRead), result.Filesystem.AllowRead)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("defaultDenyRead nil defaults to true", func(t *testing.T) {
|
||||
base := &Config{
|
||||
Filesystem: FilesystemConfig{},
|
||||
}
|
||||
result := Merge(base, nil)
|
||||
if !result.Filesystem.IsDefaultDenyRead() {
|
||||
t.Error("expected IsDefaultDenyRead() to be true when nil (deny-by-default)")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("defaultDenyRead explicit false overrides", func(t *testing.T) {
|
||||
base := &Config{
|
||||
Filesystem: FilesystemConfig{
|
||||
DefaultDenyRead: boolPtr(true),
|
||||
},
|
||||
}
|
||||
override := &Config{
|
||||
Filesystem: FilesystemConfig{
|
||||
DefaultDenyRead: boolPtr(false),
|
||||
},
|
||||
}
|
||||
result := Merge(base, override)
|
||||
if result.Filesystem.IsDefaultDenyRead() {
|
||||
t.Error("expected IsDefaultDenyRead() to be false (override explicit false)")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func boolPtr(b bool) *bool {
|
||||
|
||||
Reference in New Issue
Block a user