merge: integrate proxyctl ticket 07

# Conflicts:
#	cmd/proxyctl/main.go
#	internal/securefs/securefs.go
This commit is contained in:
proxyctl coordinator
2026-07-17 03:53:04 +08:00
15 changed files with 1242 additions and 53 deletions
+45 -1
View File
@@ -14,6 +14,8 @@ import (
"path/filepath"
"strings"
"golang.org/x/sys/unix"
"proxyctl/internal/profile"
"proxyctl/internal/securefs"
"proxyctl/internal/target"
@@ -188,6 +190,46 @@ func (a *Adapter) EnvironmentPath() string { return a.configuration.Environment
func (a *Adapter) RecoveryPath() string { return a.configuration.RecoveryFile }
func (a *Adapter) StartupPath() string { return a.configuration.StartupFile }
// DiagnosticChecks performs no writes or command execution.
func (a *Adapter) DiagnosticChecks(_ context.Context, _ *profile.Profile) []target.Check {
paths := []string{a.configuration.EnvironmentFile, a.configuration.RecoveryFile}
writable := true
var evidence []string
for _, path := range paths {
ok, detail := writablePath(path)
writable = writable && ok
evidence = append(evidence, detail)
}
return []target.Check{
{Name: "required commands", OK: true, Evidence: "Shell Target requires no external commands"},
{Name: "write authority", OK: writable, Evidence: strings.Join(evidence, "; ")},
}
}
func writablePath(path string) (bool, string) {
candidate := path
for {
info, err := os.Stat(candidate)
if err == nil {
if !info.IsDir() {
candidate = filepath.Dir(candidate)
}
if err := unix.Access(candidate, unix.W_OK); err != nil {
return false, candidate + " is not writable: " + err.Error()
}
return true, candidate + " is writable"
}
if !errors.Is(err, fs.ErrNotExist) {
return false, candidate + " cannot be inspected: " + err.Error()
}
parent := filepath.Dir(candidate)
if parent == candidate {
return false, path + " has no writable existing ancestor"
}
candidate = parent
}
}
func (a *Adapter) Inspect(_ context.Context, active *profile.Profile) target.Inspection {
metadata, managed, metadataErr := a.readRecovery()
generated, generatedErr := a.files.ReadFile(a.configuration.EnvironmentFile)
@@ -215,7 +257,9 @@ func (a *Adapter) Inspect(_ context.Context, active *profile.Profile) target.Ins
}
func (a *Adapter) SetDesired(ctx context.Context, enabled bool, active *profile.Profile) target.OperationResult {
if !a.Availability(ctx).Available {
// Enabling requires a supported current Shell, but disabling must remain
// available so reset can remove managed state after the user's Shell changes.
if enabled && !a.Availability(ctx).Available {
return target.OperationResult{Inspection: a.Inspect(ctx, active), Err: errors.New("Shell Target is unavailable: Bash or Zsh was not detected")}
}
if enabled && active == nil {
+15
View File
@@ -472,6 +472,21 @@ func TestRecoveryAndGeneratedFilesAreAtomicOwnerOnly(t *testing.T) {
}
}
func TestDisableManagedShellAfterCurrentShellBecomesUnsupported(t *testing.T) {
root := t.TempDir()
configuration := shell.Configuration{Shell: "/bin/bash", StartupFile: filepath.Join(root, ".bashrc"), EnvironmentFile: filepath.Join(root, "config", "shell.env"), RecoveryFile: filepath.Join(root, "state", "shell.json")}
enabled := shell.New(configuration, shell.OSFileSystem{}, shell.MapEnvironment{})
if result := enabled.SetDesired(context.Background(), true, activeProfile()); result.Err != nil {
t.Fatal(result.Err)
}
configuration.Shell = "/bin/fish"
unsupported := shell.New(configuration, shell.OSFileSystem{}, shell.MapEnvironment{})
result := unsupported.SetDesired(context.Background(), false, nil)
if result.Err != nil || result.Inspection.State != target.Disabled {
t.Fatalf("disable from unsupported Shell = %#v", result)
}
}
func activeProfile() *profile.Profile {
return &profile.Profile{Name: "office", Mode: profile.AdvancedMode, HTTPURL: "http://alice:secret@proxy.example:8080", HTTPSURL: "https://proxy.example:8443", AllURL: "socks5h://proxy.example:1080", Bypass: []string{"localhost", ".example.com", "10.0.0.0/8"}}
}