Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
61 changes: 2 additions & 59 deletions cmd/agency/command.go
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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
}
49 changes: 49 additions & 0 deletions cmd/agency/command_shared.go
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 2 additions & 0 deletions cmd/agency/command_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !windows

package agency

import (
Expand Down
67 changes: 67 additions & 0 deletions cmd/agency/command_windows_test.go
Original file line number Diff line number Diff line change
@@ -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
}
5 changes: 5 additions & 0 deletions cmd/agency/doc.go
Original file line number Diff line number Diff line change
@@ -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
33 changes: 2 additions & 31 deletions cmd/agency/peer.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !windows

package agency

import (
Expand All @@ -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 {
Expand Down
34 changes: 34 additions & 0 deletions cmd/agency/peer_command.go
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 2 additions & 0 deletions cmd/agency/peer_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !windows

package agency

import (
Expand Down
5 changes: 5 additions & 0 deletions cmd/agency/platform_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
//go:build !windows

package agency

const platformAgencyNotice = ""
19 changes: 19 additions & 0 deletions cmd/agency/platform_windows.go
Original file line number Diff line number Diff line change
@@ -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 }
14 changes: 2 additions & 12 deletions cmd/agency/serve.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !windows

package agency

import (
Expand All @@ -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 {
Expand Down
15 changes: 15 additions & 0 deletions cmd/agency/serve_command.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading