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
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