Skip to content
Open
18 changes: 17 additions & 1 deletion .github/workflows/frontend-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ name: Desktop release
# Generates a GitHub Release (draft) with installers + update manifests.
# Triggered by a `desktop-v*` tag or manually.
#
# Each target OS builds on its own runner so the bundled `ao` daemon is compiled
# natively for that platform. build-daemon.mjs keys the binary off the build
# host's platform, so cross-OS packaging (e.g. building the Windows installer on
# macOS) would ship a non-Windows binary named `ao` and the app could not launch
# the daemon (issues #235/#256). The per-OS matrix keeps host == target.
#
# ⚠️ Until macOS code signing + notarization secrets are configured (see
# frontend/docs/desktop-release.md), published builds are UNSIGNED and will
# NOT auto-update on macOS. The workflow still produces installable artifacts.
Expand All @@ -16,7 +22,11 @@ on:

jobs:
release:
runs-on: macos-latest
strategy:
fail-fast: false
matrix:
os: [macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
permissions:
contents: write
defaults:
Expand All @@ -29,6 +39,12 @@ jobs:
node-version: 20
cache: npm
cache-dependency-path: frontend/package-lock.json
# The daemon is compiled by build-daemon.mjs during prepackage/premake, so
# the Go toolchain must be present and pinned on every runner.
- uses: actions/setup-go@v5
with:
go-version-file: backend/go.mod
cache-dependency-path: backend/go.sum
- run: npm ci
- name: Publish
run: npm run publish
Expand Down
5 changes: 4 additions & 1 deletion backend/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/aoagents/agent-orchestrator/backend
go 1.25.7

require (
github.com/aymanbagabas/go-pty v0.2.3
github.com/coder/websocket v1.8.14
github.com/creack/pty v1.1.24
github.com/go-chi/chi/v5 v5.1.0
Expand All @@ -12,7 +13,7 @@ require (
github.com/spf13/pflag v1.0.9
github.com/swaggest/jsonschema-go v0.3.79
github.com/swaggest/openapi-go v0.2.61
golang.org/x/sys v0.43.0
golang.org/x/sys v0.44.0
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.51.0
)
Expand All @@ -26,7 +27,9 @@ require (
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
github.com/swaggest/refl v1.4.0 // indirect
github.com/u-root/u-root v0.16.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/tools v0.43.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
Expand Down
16 changes: 14 additions & 2 deletions backend/go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/aymanbagabas/go-pty v0.2.3 h1:hsqcTIUV8I4iTSh3HQl61CR2wh0YPS6gHOYLhAfWu/E=
github.com/aymanbagabas/go-pty v0.2.3/go.mod h1:GLkgQovzqN5A1xMB79yHWiG1rhcquZCjkwKQGKFPdPg=
github.com/bool64/dev v0.2.43 h1:yQ7qiZVef6WtCl2vDYU0Y+qSq+0aBrQzY8KXkklk9cQ=
github.com/bool64/dev v0.2.43/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E=
Expand All @@ -19,6 +21,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hugelgupf/vmtest v0.0.0-20240307030256-5d9f3d34a58d h1:nP8SfQJqruIVSWYJTuYc37jLHEY1Z0fF+zKSrs3K/C8=
github.com/hugelgupf/vmtest v0.0.0-20240307030256-5d9f3d34a58d/go.mod h1:B63hDJMhTupLWCHwopAyEo7wRFowx9kOc8m8j1sfOqE=
github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc=
github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
Expand Down Expand Up @@ -54,18 +58,26 @@ github.com/swaggest/openapi-go v0.2.61 h1:psc+LE7pWhEjmJpmkti9tUmBPkkobdUNflBf5P
github.com/swaggest/openapi-go v0.2.61/go.mod h1:786CwSwleh1IorB0nfwYGESWf83JgQh6fBc1PeJe4Iw=
github.com/swaggest/refl v1.4.0 h1:CftOSdTqRqs100xpFOT/Rifss5xBV/CT0S/FN60Xe9k=
github.com/swaggest/refl v1.4.0/go.mod h1:4uUVFVfPJ0NSX9FPwMPspeHos9wPFlCMGoPRllUbpvA=
github.com/u-root/gobusybox/src v0.0.0-20250101170133-2e884e4509c7 h1:dtiVT4SeBUc/vHtwI2HjDZN+FCKTstQBxugIxJEGo9g=
github.com/u-root/gobusybox/src v0.0.0-20250101170133-2e884e4509c7/go.mod h1:PW3wGFCHjdHxAhra5FKvcARbCGqGfentYuPKmuhv8DY=
github.com/u-root/u-root v0.16.0 h1:wY40O83MBVks97+Is0WlFlOPSwKQMIrWP9R1IsrExg8=
github.com/u-root/u-root v0.16.0/go.mod h1:yL/XdSSW27PdGLgUh4MNRBy54mKM+TBLzpwiB4nwj90=
github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
Expand Down
40 changes: 35 additions & 5 deletions backend/internal/adapters/agent/codex/codex.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,13 @@ func (p *Plugin) GetLaunchCommand(ctx context.Context, cfg ports.LaunchConfig) (
appendHookTrustBypassFlag(&cmd)
appendApprovalFlags(&cmd, cfg.Permissions)
appendSessionHookFlags(&cmd)
appendTerminalCompatibilityFlags(&cmd)
appendWorkspaceTrustFlag(&cmd, cfg.WorkspacePath)

if cfg.SystemPromptFile != "" {
cmd = append(cmd, "-c", "model_instructions_file="+cfg.SystemPromptFile)
} else if cfg.SystemPrompt != "" {
cmd = append(cmd, "-c", "developer_instructions="+cfg.SystemPrompt)
cmd = append(cmd, "-c", "developer_instructions="+codexTOMLConfigString(cfg.SystemPrompt))
}

if cfg.Prompt != "" {
Expand Down Expand Up @@ -122,6 +123,7 @@ func (p *Plugin) GetRestoreCommand(ctx context.Context, cfg ports.RestoreConfig)
appendHookTrustBypassFlag(&cmd)
appendApprovalFlags(&cmd, cfg.Permissions)
appendSessionHookFlags(&cmd)
appendTerminalCompatibilityFlags(&cmd)
appendWorkspaceTrustFlag(&cmd, cfg.Session.WorkspacePath)
cmd = append(cmd, agentSessionID)
return cmd, true, nil
Expand Down Expand Up @@ -154,10 +156,10 @@ func ResolveCodexBinary(ctx context.Context) (string, error) {
}

if runtime.GOOS == "windows" {
for _, name := range []string{"codex.cmd", "codex.exe", "codex"} {
for _, name := range []string{"codex.exe", "codex.cmd", "codex"} {
path, err := exec.LookPath(name)
if err == nil && path != "" {
return path, nil
return resolveNativeWindowsCodex(path), nil
}
if err := ctx.Err(); err != nil {
return "", err
Expand All @@ -166,17 +168,19 @@ func ResolveCodexBinary(ctx context.Context) (string, error) {

candidates := []string{}
if appData := os.Getenv("APPDATA"); appData != "" {
shim := filepath.Join(appData, "npm", "codex.cmd")
candidates = append(candidates, windowsNativeCodexCandidatesForShim(shim)...)
candidates = append(candidates,
filepath.Join(appData, "npm", "codex.cmd"),
filepath.Join(appData, "npm", "codex.exe"),
shim,
)
}
if home, err := os.UserHomeDir(); err == nil {
candidates = append(candidates, filepath.Join(home, ".cargo", "bin", "codex.exe"))
}
for _, candidate := range candidates {
if fileExists(candidate) {
return candidate, nil
return resolveNativeWindowsCodex(candidate), nil
}
if err := ctx.Err(); err != nil {
return "", err
Expand Down Expand Up @@ -213,6 +217,26 @@ func ResolveCodexBinary(ctx context.Context) (string, error) {
return "", fmt.Errorf("codex: %w", ports.ErrAgentBinaryNotFound)
}

func resolveNativeWindowsCodex(path string) string {
if runtime.GOOS != "windows" || !strings.EqualFold(filepath.Ext(path), ".cmd") {
return path
}
for _, candidate := range windowsNativeCodexCandidatesForShim(path) {
if fileExists(candidate) {
return candidate
}
}
return path
}

func windowsNativeCodexCandidatesForShim(shim string) []string {
dir := filepath.Dir(shim)
return []string{
filepath.Join(dir, "node_modules", "@openai", "codex", "node_modules", "@openai", "codex-win32-x64", "vendor", "x86_64-pc-windows-msvc", "bin", "codex.exe"),
filepath.Join(dir, "node_modules", "@openai", "codex", "bin", "codex.exe"),
}
}

func (p *Plugin) codexBinary(ctx context.Context) (string, error) {
p.binaryMu.Lock()
defer p.binaryMu.Unlock()
Expand Down Expand Up @@ -270,6 +294,12 @@ func appendHookTrustBypassFlag(cmd *[]string) {
*cmd = append(*cmd, "--dangerously-bypass-hook-trust")
}

func appendTerminalCompatibilityFlags(cmd *[]string) {
if runtime.GOOS == "windows" {
*cmd = append(*cmd, "--no-alt-screen")
}
}

func appendApprovalFlags(cmd *[]string, permissions ports.PermissionMode) {
switch normalizePermissionMode(permissions) {
case ports.PermissionModeDefault:
Expand Down
16 changes: 11 additions & 5 deletions backend/internal/adapters/agent/codex/codex_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,11 @@ func TestGetLaunchCommandBuildsCrossPlatformArgv(t *testing.T) {
"--dangerously-bypass-approvals-and-sandbox",
}
want = append(want, sessionHookFlags()...)
if runtime.GOOS == "windows" {
want = append(want, "--no-alt-screen")
}
want = append(want,
"-c", `projects={"`+workspace+`"={trust_level="trusted"}}`,
"-c", `projects={`+codexTOMLConfigString(workspace)+`={trust_level="trusted"}}`,
"-c", "model_instructions_file="+filepath.Join("tmp", "prompt with spaces.md"),
"--", "-fix this",
)
Expand Down Expand Up @@ -158,15 +161,15 @@ func TestAppendWorkspaceTrustFlagCoversLiteralAndResolvedPaths(t *testing.T) {
appendWorkspaceTrustFlag(&cmd, link)
want := []string{
"-c",
`projects={"` + link + `"={trust_level="trusted"},"` + target + `"={trust_level="trusted"}}`,
`projects={'` + link + `'={trust_level="trusted"},'` + target + `'={trust_level="trusted"}}`,
}
if !reflect.DeepEqual(cmd, want) {
t.Fatalf("trust flag\nwant: %#v\n got: %#v", want, cmd)
}

cmd = nil
appendWorkspaceTrustFlag(&cmd, target)
want = []string{"-c", `projects={"` + target + `"={trust_level="trusted"}}`}
want = []string{"-c", `projects={'` + target + `'={trust_level="trusted"}}`}
if !reflect.DeepEqual(cmd, want) {
t.Fatalf("canonical-path trust flag\nwant: %#v\n got: %#v", want, cmd)
}
Expand Down Expand Up @@ -415,8 +418,11 @@ func TestGetRestoreCommandReadsAgentSessionID(t *testing.T) {
"-c", `approvals_reviewer="auto_review"`,
}
want = append(want, sessionHookFlags()...)
if runtime.GOOS == "windows" {
want = append(want, "--no-alt-screen")
}
want = append(want,
"-c", `projects={"`+workspace+`"={trust_level="trusted"}}`,
"-c", `projects={`+codexTOMLConfigString(workspace)+`={trust_level="trusted"}}`,
"thread-123",
)
if !reflect.DeepEqual(cmd, want) {
Expand Down Expand Up @@ -566,7 +572,7 @@ func TestDoctorLaunchProbesMirrorLaunchFlags(t *testing.T) {
for _, want := range []string{
"hooks.SessionStart=", "hooks.UserPromptSubmit=", "hooks.PermissionRequest=", "hooks.Stop=",
"notice.hide_rate_limit_model_nudge=true",
`projects={"`,
`projects={`,
} {
if !strings.Contains(joined, want) {
t.Fatalf("override probe missing %q in %s", want, joined)
Expand Down
22 changes: 21 additions & 1 deletion backend/internal/adapters/agent/codex/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,22 @@ func appendWorkspaceTrustFlag(cmd *[]string, workspacePath string) {
}
entries := make([]string, 0, len(keys))
for _, key := range keys {
entries = append(entries, codexTOMLBasicString(key)+`={trust_level="trusted"}`)
entries = append(entries, codexTOMLConfigString(key)+`={trust_level="trusted"}`)
}
*cmd = append(*cmd, "-c", "projects={"+strings.Join(entries, ",")+"}")
}

func codexTOMLConfigString(s string) string {
if !containsTOMLControl(s) && !strings.Contains(s, "'") {
return codexTOMLLiteralString(s)
}
return codexTOMLBasicString(s)
}

func codexTOMLLiteralString(s string) string {
return "'" + s + "'"
}

// codexTOMLBasicString renders s as a TOML basic string, escaping backslashes
// and quotes (Windows paths) plus control characters so the value survives
// Codex's TOML parse of the `-c` override.
Expand All @@ -132,6 +143,15 @@ func codexTOMLBasicString(s string) string {
return b.String()
}

func containsTOMLControl(s string) bool {
for _, r := range s {
if r < 0x20 || r == 0x7f {
return true
}
}
return false
}

// GetAgentHooks no longer installs workspace files — Codex never loads them
// from AO's worktrees (see the package comment above); the hooks ride the
// launch command instead. It still strips hook entries that older AO versions
Expand Down
Loading
Loading