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