From 25f098e5766d79e8645267282c3216950cfb14bd Mon Sep 17 00:00:00 2001 From: Grivn Date: Sat, 15 Aug 2026 07:59:32 +0800 Subject: [PATCH] fix: disable Agency on Windows Keep the Memory product buildable on Windows while preserving the Agency command shape for help and version discovery. Operational Agency commands now fail closed before touching state, and Windows CI covers the product and command boundary.\n\nValidated with make test, native build, Windows cross-build, and Windows command test compilation. --- .github/workflows/ci.yml | 22 ++++++++++ README.md | 5 ++- cmd/agency/command.go | 61 +-------------------------- cmd/agency/command_shared.go | 49 ++++++++++++++++++++++ cmd/agency/command_test.go | 2 + cmd/agency/command_windows_test.go | 67 ++++++++++++++++++++++++++++++ cmd/agency/doc.go | 5 +++ cmd/agency/peer.go | 33 +-------------- cmd/agency/peer_command.go | 34 +++++++++++++++ cmd/agency/peer_test.go | 2 + cmd/agency/platform_unix.go | 5 +++ cmd/agency/platform_windows.go | 19 +++++++++ cmd/agency/serve.go | 14 +------ cmd/agency/serve_command.go | 15 +++++++ cmd/agency/serve_test.go | 2 + cmd/agency/setup.go | 19 +-------- cmd/agency/setup_command.go | 20 +++++++++ cmd/agency/setup_test.go | 2 + cmd/agency/terminal_unix.go | 19 +++++++++ docs/zh/README.md | 5 ++- 20 files changed, 279 insertions(+), 121 deletions(-) create mode 100644 cmd/agency/command_shared.go create mode 100644 cmd/agency/command_windows_test.go create mode 100644 cmd/agency/doc.go create mode 100644 cmd/agency/peer_command.go create mode 100644 cmd/agency/platform_unix.go create mode 100644 cmd/agency/platform_windows.go create mode 100644 cmd/agency/serve_command.go create mode 100644 cmd/agency/setup_command.go create mode 100644 cmd/agency/terminal_unix.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 08c006fc..1b9457dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,3 +32,25 @@ jobs: - name: Run deterministic test suite run: make test + + windows-memory: + runs-on: windows-latest + timeout-minutes: 15 + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache-dependency-path: go.sum + + - name: Build Windows Memory product + run: go build -o mnemon.exe . + + - name: Test Windows command boundary + run: go test ./cmd ./cmd/agency -count=1 diff --git a/README.md b/README.md index 31b6f0cc..84c91569 100644 --- a/README.md +++ b/README.md @@ -65,12 +65,15 @@ See [Design & Architecture](docs/DESIGN.md) for details. brew install --cask mnemon-dev/tap/mnemon ``` -**Go install** (macOS / Linux): +**Go install** (macOS / Linux / Windows): ```bash go install github.com/mnemon-dev/mnemon@latest ``` +Windows supports the core Memory commands. Agency remains unavailable on +Windows until its local authority boundary has native Windows security. + **From source** (macOS / Linux): ```bash diff --git a/cmd/agency/command.go b/cmd/agency/command.go index 4ed0675f..7a4946c1 100644 --- a/cmd/agency/command.go +++ b/cmd/agency/command.go @@ -1,50 +1,10 @@ -// Package agency declares the mnemon agency command tree. -// -// Commands compose existing Agency services. Canonical state and admission -// remain owned by internal packages. package agency -import ( - "errors" - - "github.com/mnemon-dev/mnemon/internal/agency/client" - "github.com/mnemon-dev/mnemon/internal/daemon" - "github.com/spf13/cobra" -) - -type commandFailure struct { - code int - err error -} - -func (failure commandFailure) Error() string { - if failure.err == nil { - return "" - } - return failure.err.Error() -} - -// ExitCode reports the process status carried by an Agency command failure. -// Ordinary Cobra validation errors are intentionally not classified here. -func ExitCode(err error) (int, bool) { - var failure commandFailure - if !errors.As(err, &failure) { - return 0, false - } - return failure.code, true -} +import "github.com/spf13/cobra" // New returns a fresh Agency command tree for the Mnemon product root. func New(version string) *cobra.Command { - command := &cobra.Command{ - Use: "agency", - Short: "Manage durable Agent work and peer collaboration", - Long: "Mnemon Agency adds durable project-local responsibility and admitted effects to an existing Agent Runtime.", - Version: version, - Args: cobra.NoArgs, - RunE: showCommandHelp, - } - command.SetVersionTemplate("mnemon agency version {{.Version}}\n") + command := newCommand(version) command.AddCommand(setupCommand(), peerCommand(), serveCommand()) // These machine surfaces keep the exact grammar owned by agencyclient. @@ -58,20 +18,3 @@ func New(version string) *cobra.Command { } return command } - -func showCommandHelp(command *cobra.Command, _ []string) error { - if err := command.Help(); err != nil { - return commandFailure{code: 1, err: err} - } - return nil -} - -func runTerminal(command *cobra.Command, args []string) error { - code := agencyclient.Run(command.Context(), append([]string{command.Name()}, args...), - command.InOrStdin(), command.OutOrStdout(), command.ErrOrStderr(), daemon.Ensure) - if code != 0 { - // agencyclient has already emitted the bounded machine diagnostic. - return commandFailure{code: code} - } - return nil -} diff --git a/cmd/agency/command_shared.go b/cmd/agency/command_shared.go new file mode 100644 index 00000000..34cfc010 --- /dev/null +++ b/cmd/agency/command_shared.go @@ -0,0 +1,49 @@ +package agency + +import ( + "errors" + + "github.com/spf13/cobra" +) + +type commandFailure struct { + code int + err error +} + +func (failure commandFailure) Error() string { + if failure.err == nil { + return "" + } + return failure.err.Error() +} + +// ExitCode reports the process status carried by an Agency command failure. +// Ordinary Cobra validation errors are intentionally not classified here. +func ExitCode(err error) (int, bool) { + var failure commandFailure + if !errors.As(err, &failure) { + return 0, false + } + return failure.code, true +} + +func newCommand(version string) *cobra.Command { + command := &cobra.Command{ + Use: "agency", + Short: "Manage durable Agent work and peer collaboration", + Long: "Mnemon Agency adds durable project-local responsibility and admitted effects to an existing Agent Runtime." + platformAgencyNotice, + Version: version, + Args: cobra.NoArgs, + RunE: showCommandHelp, + } + command.SetVersionTemplate("mnemon agency version {{.Version}}\n") + return command +} + +func showCommandHelp(command *cobra.Command, _ []string) error { + if err := command.Help(); err != nil { + return commandFailure{code: 1, err: err} + } + return nil +} diff --git a/cmd/agency/command_test.go b/cmd/agency/command_test.go index fdd6dabb..a41852f1 100644 --- a/cmd/agency/command_test.go +++ b/cmd/agency/command_test.go @@ -1,3 +1,5 @@ +//go:build !windows + package agency import ( diff --git a/cmd/agency/command_windows_test.go b/cmd/agency/command_windows_test.go new file mode 100644 index 00000000..e7b34324 --- /dev/null +++ b/cmd/agency/command_windows_test.go @@ -0,0 +1,67 @@ +//go:build windows + +package agency + +import ( + "bytes" + "context" + "fmt" + "strings" + "testing" + + "github.com/spf13/cobra" +) + +func TestWindowsAgencyKeepsHelpAndVersionDiscoverable(t *testing.T) { + for _, test := range []struct { + args []string + want string + }{ + {args: []string{"--help"}, want: "Agency operations are not supported on Windows."}, + {args: []string{"--version"}, want: "mnemon agency version test-version\n"}, + {args: []string{"peer", "prepare", "--help"}, want: "--advertise"}, + } { + stdout, stderr, exit := executeWindowsAgency(test.args, "test-version") + if exit != 0 || stderr != "" || !strings.Contains(stdout, test.want) { + t.Fatalf("Run(%q) = exit %d stdout %q stderr %q", test.args, exit, stdout, stderr) + } + } +} + +func TestWindowsAgencyRejectsEveryOperationalCommand(t *testing.T) { + for _, args := range [][]string{ + {"setup"}, + {"peer", "prepare"}, + {"peer", "enroll"}, + {"serve"}, + {"hook", "attach"}, + {"agent", "current"}, + {"artifact", "read"}, + } { + stdout, stderr, exit := executeWindowsAgency(args, "dev") + if exit != 2 || stdout != "" || stderr != errUnsupported.Error()+"\n" { + t.Errorf("Run(%q) = exit %d stdout %q stderr %q", args, exit, stdout, stderr) + } + } +} + +func executeWindowsAgency(args []string, version string) (string, string, int) { + var stdout, stderr bytes.Buffer + root := &cobra.Command{Use: "mnemon", SilenceErrors: true, SilenceUsage: true} + root.AddCommand(New(version)) + root.SetArgs(append([]string{"agency"}, args...)) + root.SetIn(strings.NewReader("")) + root.SetOut(&stdout) + root.SetErr(&stderr) + _, err := root.ExecuteContextC(context.Background()) + if err == nil { + return stdout.String(), stderr.String(), 0 + } + if err.Error() != "" { + _, _ = fmt.Fprintln(&stderr, err) + } + if code, ok := ExitCode(err); ok { + return stdout.String(), stderr.String(), code + } + return stdout.String(), stderr.String(), 2 +} diff --git a/cmd/agency/doc.go b/cmd/agency/doc.go new file mode 100644 index 00000000..e5eec7c9 --- /dev/null +++ b/cmd/agency/doc.go @@ -0,0 +1,5 @@ +// Package agency declares the mnemon agency command tree. +// +// Commands compose existing Agency services. Canonical state and admission +// remain owned by internal packages. +package agency diff --git a/cmd/agency/peer.go b/cmd/agency/peer.go index 62d1fcd3..8788d876 100644 --- a/cmd/agency/peer.go +++ b/cmd/agency/peer.go @@ -1,3 +1,5 @@ +//go:build !windows + package agency import ( @@ -14,37 +16,6 @@ import ( const maxPeerCardInputBytes = 1025 -func peerCommand() *cobra.Command { - command := &cobra.Command{ - Use: "peer", - Short: "Configure explicit peer exchange", - Args: cobra.NoArgs, - RunE: showCommandHelp, - } - - prepare := &cobra.Command{ - Use: "prepare", - Short: "Prepare this project's peer identity and addresses", - Args: cobra.NoArgs, - RunE: runPeerPrepare, - } - prepare.Flags().Var(new(singleString), "listen", "local HOST:PORT to listen on") - prepare.Flags().Var(new(singleString), "advertise", "reachable HOST:PORT advertised to peers") - prepare.Flags().Var(new(singleString), "project-root", "project root (default: current directory)") - - enroll := &cobra.Command{ - Use: "enroll", - Short: "Enroll one peer from its Peer Card on stdin", - Args: cobra.NoArgs, - RunE: runPeerEnroll, - } - enroll.Flags().Var(new(singleString), "alias", "stable local alias for the peer") - enroll.Flags().Var(new(singleString), "project-root", "project root (default: current directory)") - - command.AddCommand(prepare, enroll) - return command -} - func runPeerPrepare(command *cobra.Command, _ []string) error { listenAddress, err := command.Flags().GetString("listen") if err != nil { diff --git a/cmd/agency/peer_command.go b/cmd/agency/peer_command.go new file mode 100644 index 00000000..ca190e93 --- /dev/null +++ b/cmd/agency/peer_command.go @@ -0,0 +1,34 @@ +package agency + +import "github.com/spf13/cobra" + +func peerCommand() *cobra.Command { + command := &cobra.Command{ + Use: "peer", + Short: "Configure explicit peer exchange", + Args: cobra.NoArgs, + RunE: showCommandHelp, + } + + prepare := &cobra.Command{ + Use: "prepare", + Short: "Prepare this project's peer identity and addresses", + Args: cobra.NoArgs, + RunE: runPeerPrepare, + } + prepare.Flags().Var(new(singleString), "listen", "local HOST:PORT to listen on") + prepare.Flags().Var(new(singleString), "advertise", "reachable HOST:PORT advertised to peers") + prepare.Flags().Var(new(singleString), "project-root", "project root (default: current directory)") + + enroll := &cobra.Command{ + Use: "enroll", + Short: "Enroll one peer from its Peer Card on stdin", + Args: cobra.NoArgs, + RunE: runPeerEnroll, + } + enroll.Flags().Var(new(singleString), "alias", "stable local alias for the peer") + enroll.Flags().Var(new(singleString), "project-root", "project root (default: current directory)") + + command.AddCommand(prepare, enroll) + return command +} diff --git a/cmd/agency/peer_test.go b/cmd/agency/peer_test.go index 1d4da15d..4f17f81c 100644 --- a/cmd/agency/peer_test.go +++ b/cmd/agency/peer_test.go @@ -1,3 +1,5 @@ +//go:build !windows + package agency import ( diff --git a/cmd/agency/platform_unix.go b/cmd/agency/platform_unix.go new file mode 100644 index 00000000..0b8a5a59 --- /dev/null +++ b/cmd/agency/platform_unix.go @@ -0,0 +1,5 @@ +//go:build !windows + +package agency + +const platformAgencyNotice = "" diff --git a/cmd/agency/platform_windows.go b/cmd/agency/platform_windows.go new file mode 100644 index 00000000..09050b0e --- /dev/null +++ b/cmd/agency/platform_windows.go @@ -0,0 +1,19 @@ +//go:build windows + +package agency + +import ( + "errors" + + "github.com/spf13/cobra" +) + +const platformAgencyNotice = " Agency operations are not supported on Windows." + +var errUnsupported = errors.New("mnemon agency is not supported on Windows") + +func runSetup(*cobra.Command, []string) error { return errUnsupported } +func runPeerPrepare(*cobra.Command, []string) error { return errUnsupported } +func runPeerEnroll(*cobra.Command, []string) error { return errUnsupported } +func runServe(*cobra.Command, []string) error { return errUnsupported } +func runTerminal(*cobra.Command, []string) error { return errUnsupported } diff --git a/cmd/agency/serve.go b/cmd/agency/serve.go index 08265cf7..e6d00d97 100644 --- a/cmd/agency/serve.go +++ b/cmd/agency/serve.go @@ -1,3 +1,5 @@ +//go:build !windows + package agency import ( @@ -17,18 +19,6 @@ import ( const gracefulShutdownBudget = 5 * time.Second -func serveCommand() *cobra.Command { - command := &cobra.Command{ - Use: "serve", - Short: "Serve one already-provisioned Agency authority", - Args: cobra.NoArgs, - RunE: runServe, - } - command.Flags().Var(new(singleString), "state-dir", - "already-provisioned Agency state directory") - return command -} - func runServe(command *cobra.Command, _ []string) error { stateDirectory, err := command.Flags().GetString("state-dir") if err != nil { diff --git a/cmd/agency/serve_command.go b/cmd/agency/serve_command.go new file mode 100644 index 00000000..e1091d23 --- /dev/null +++ b/cmd/agency/serve_command.go @@ -0,0 +1,15 @@ +package agency + +import "github.com/spf13/cobra" + +func serveCommand() *cobra.Command { + command := &cobra.Command{ + Use: "serve", + Short: "Serve one already-provisioned Agency authority", + Args: cobra.NoArgs, + RunE: runServe, + } + command.Flags().Var(new(singleString), "state-dir", + "already-provisioned Agency state directory") + return command +} diff --git a/cmd/agency/serve_test.go b/cmd/agency/serve_test.go index d1ff9795..e448c4e7 100644 --- a/cmd/agency/serve_test.go +++ b/cmd/agency/serve_test.go @@ -1,3 +1,5 @@ +//go:build !windows + package agency import ( diff --git a/cmd/agency/setup.go b/cmd/agency/setup.go index bad0ade6..ef5febb6 100644 --- a/cmd/agency/setup.go +++ b/cmd/agency/setup.go @@ -1,3 +1,5 @@ +//go:build !windows + package agency import ( @@ -13,23 +15,6 @@ import ( "github.com/spf13/cobra" ) -const setupRuntimePi = "pi" - -func setupCommand() *cobra.Command { - runtime := &singleString{value: setupRuntimePi} - projectRoot := new(singleString) - command := &cobra.Command{ - Use: "setup", - Short: "Set up Agency for this project", - Long: "Provision project-local Agency state, ensure its daemon, and install the Pi integration.", - Args: cobra.NoArgs, - RunE: runSetup, - } - command.Flags().Var(runtime, "runtime", "Agent Runtime to integrate (pi)") - command.Flags().Var(projectRoot, "project-root", "project root (default: current directory)") - return command -} - func runSetup(command *cobra.Command, _ []string) error { runtime, err := command.Flags().GetString("runtime") if err != nil { diff --git a/cmd/agency/setup_command.go b/cmd/agency/setup_command.go new file mode 100644 index 00000000..f3b89ee9 --- /dev/null +++ b/cmd/agency/setup_command.go @@ -0,0 +1,20 @@ +package agency + +import "github.com/spf13/cobra" + +const setupRuntimePi = "pi" + +func setupCommand() *cobra.Command { + runtime := &singleString{value: setupRuntimePi} + projectRoot := new(singleString) + command := &cobra.Command{ + Use: "setup", + Short: "Set up Agency for this project", + Long: "Provision project-local Agency state, ensure its daemon, and install the Pi integration.", + Args: cobra.NoArgs, + RunE: runSetup, + } + command.Flags().Var(runtime, "runtime", "Agent Runtime to integrate (pi)") + command.Flags().Var(projectRoot, "project-root", "project root (default: current directory)") + return command +} diff --git a/cmd/agency/setup_test.go b/cmd/agency/setup_test.go index dd6e1c8a..6df08168 100644 --- a/cmd/agency/setup_test.go +++ b/cmd/agency/setup_test.go @@ -1,3 +1,5 @@ +//go:build !windows + package agency import ( diff --git a/cmd/agency/terminal_unix.go b/cmd/agency/terminal_unix.go new file mode 100644 index 00000000..cb1474be --- /dev/null +++ b/cmd/agency/terminal_unix.go @@ -0,0 +1,19 @@ +//go:build !windows + +package agency + +import ( + "github.com/mnemon-dev/mnemon/internal/agency/client" + "github.com/mnemon-dev/mnemon/internal/daemon" + "github.com/spf13/cobra" +) + +func runTerminal(command *cobra.Command, args []string) error { + code := agencyclient.Run(command.Context(), append([]string{command.Name()}, args...), + command.InOrStdin(), command.OutOrStdout(), command.ErrOrStderr(), daemon.Ensure) + if code != 0 { + // agencyclient has already emitted the bounded machine diagnostic. + return commandFailure{code: code} + } + return nil +} diff --git a/docs/zh/README.md b/docs/zh/README.md index 762f929f..5a5a68d9 100644 --- a/docs/zh/README.md +++ b/docs/zh/README.md @@ -65,12 +65,15 @@ Mnemon 同时填补了协议栈中的空白。MCP 标准化了 LLM 如何发现 brew install --cask mnemon-dev/tap/mnemon ``` -**Go install**(macOS / Linux): +**Go install**(macOS / Linux / Windows): ```bash go install github.com/mnemon-dev/mnemon@latest ``` +Windows 支持核心 Memory 命令。Agency 的本地权威边界完成原生 Windows +安全实现前,在 Windows 上保持不可用。 + **从源码构建**(macOS / Linux): ```bash