Skip to content

Commit 08163ea

Browse files
authored
harness: wally openwork with every RunAnywhere model (#230)
* harness: add wally openwork, the OpenWork desktop app with every RunAnywhere model * openwork: start the app's own executable so the token stays out of open's arguments * openwork: write the workspace path as OpenWork resolves it, fixing the Windows e2e * openwork: address review: isolated engine data, hosted refresh and image gate, safer app discovery, stricter tests
1 parent f8803a0 commit 08163ea

55 files changed

Lines changed: 1241 additions & 14 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎docs/EDITORS.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ configure by hand:
1111
wally claude-code -m qwen3-4b-instruct-2507
1212
wally codex -m qwen3-4b-instruct-2507
1313
wally chatgpt -m qwen3-4b-instruct-2507
14+
wally openwork -m qwen3-4b-instruct-2507
1415
wally hermes -m qwen3-4b-instruct-2507
1516
wally deepseek -m qwen3-4b-instruct-2507
1617
wally openclaw -m qwen3-4b-instruct-2507
@@ -24,6 +25,7 @@ tool starts the way you already have it configured, and wally wires nothing.
2425
| `claude-code` | `ANTHROPIC_BASE_URL` and `ANTHROPIC_AUTH_TOKEN` in the process |
2526
| `codex` | `-c` overrides on its command line naming a `wally` provider at the translator, plus a model catalog file for the run; the loopback token is in `WALLY_CODEX_API_KEY`. It runs in a Codex home of wally's own, so its threads and settings stay apart from yours; nothing is copied in and `~/.codex` is never written |
2627
| `chatgpt` | the ChatGPT app for macOS started as a second instance with its own app data and its own Codex home, whose `config.toml` holds the same settings `codex` passes as `-c` (one block wally rewrites per launch; the app's own entries are kept) and an API-key `auth.json` so no ChatGPT sign-in is asked for. Your own ChatGPT app and `~/.codex` are never touched. Quitting the app ends the run, and Ctrl-C quits the app |
28+
| `openwork` | the OpenWork desktop app (public edition, `brew install --cask openwork` on macOS) started as a second instance on a profile of wally's own under the state directory (`openwork/`): its Electron data, runtime database, server settings, opencode history and a `RunAnywhere` workspace all live there, so your own OpenWork, your opencode history and `~/OpenWork Chat` are never written. Its opencode 1.x engine gets the provider in `OPENCODE_CONFIG_CONTENT`: the hosted catalog under `runanywhere` and, for a local model, `runanywhere-local` beside it, with the launched model selected. Chats stay on that engine (`OPENWORK_ENGINE_V2_PREVIEW=off`, `OPENWORK_ENGINE_AUTO_UPGRADE=0`), because OpenWork starts its opencode 2 engine with an allowlisted environment that never sees the provider. Your global `~/.config/opencode` is still read, but opencode's log and saved logins go to the profile too (`XDG_DATA_HOME`). The app's console output goes to `openwork/openwork.log`. The profile is kept between runs. Quitting the app ends the run, and Ctrl-C quits the app. On Linux only `OpenWork.AppImage` on PATH is found, never a bare `openwork` (the npm package of that name is another program); `WALLY_OPENWORK_APP` points at any other install. The Windows location (`%LOCALAPPDATA%\Programs\OpenWork\OpenWork.exe`) has not been checked against a real install yet |
2729
| `opencode` | a temporary provider in `OPENCODE_CONFIG_CONTENT`, with the local or hosted endpoint and context limits |
2830
| `claude-desktop` | a gateway profile in Claude Desktop's third party mode, covering the chat and Cowork tabs |
2931
| `hermes` | `CUSTOM_BASE_URL`, `HERMES_INFERENCE_PROVIDER=custom`, the model in `HERMES_INFERENCE_MODEL`, and the key under the name its host gates on |

‎scripts/test/e2e-openwork.sh‎

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#!/usr/bin/env bash
2+
# Real-app round trip for `wally openwork` on macOS: launches the installed
3+
# OpenWork on wally's private profile, asks its engine which models it offers,
4+
# sends one prompt per provider, quits the app and checks nothing is left
5+
# running. Opt-in: needs WALLY_E2E_OPENWORK=1, OpenWork.app, and a model
6+
# (WALLY_E2E_OPENWORK_MODEL, a local id or a hosted one on a signed-in
7+
# account). WALLY_E2E_OPENWORK_HOSTED names a hosted model to prompt as well.
8+
#
9+
# WALLY_E2E_OPENWORK=1 WALLY_E2E_OPENWORK_MODEL=qwen3-4b-instruct-2507 \
10+
# bash scripts/test/e2e-openwork.sh ./build/wally
11+
set -euo pipefail
12+
13+
WALLY="${1:?usage: e2e-openwork.sh <wally binary>}"
14+
if [[ "${WALLY_E2E_OPENWORK:-0}" != "1" ]]; then
15+
echo " skip openwork (WALLY_E2E_OPENWORK is not 1)"
16+
exit 0
17+
fi
18+
if [[ "$(uname -s)" != "Darwin" ]]; then
19+
echo " skip openwork (macOS only)"
20+
exit 0
21+
fi
22+
MODEL="${WALLY_E2E_OPENWORK_MODEL:?set WALLY_E2E_OPENWORK_MODEL}"
23+
HOSTED="${WALLY_E2E_OPENWORK_HOSTED:-}"
24+
STATE="${XDG_STATE_HOME:-${HOME}/.local/state}/runanywhere/openwork"
25+
ENGINES="${STATE}/config/engine-instances.json"
26+
LOG="$(mktemp -t wally-openwork-e2e)"
27+
28+
if pgrep -f "OpenWork.app/Contents/MacOS/OpenWork" >/dev/null; then
29+
echo " FAIL openwork: quit every OpenWork window first, so the instance this run starts can be told apart"
30+
exit 1
31+
fi
32+
33+
# Only a folder this run creates counts against it.
34+
HOME_WORKSPACE_EXISTED=0
35+
[[ -e "${HOME}/OpenWork Chat" ]] && HOME_WORKSPACE_EXISTED=1
36+
37+
rm -f "${ENGINES}"
38+
"${WALLY}" openwork -m "${MODEL}" >"${LOG}" 2>&1 &
39+
WALLY_PID=$!
40+
cleanup() {
41+
osascript -e 'tell application "OpenWork" to quit' >/dev/null 2>&1 || true
42+
for _ in $(seq 1 30); do
43+
kill -0 "${WALLY_PID}" 2>/dev/null || return 0
44+
sleep 1
45+
done
46+
echo " FAIL openwork: wally was still running 30 s after the app was asked to quit"
47+
kill "${WALLY_PID}" 2>/dev/null || true
48+
return 1
49+
}
50+
trap cleanup EXIT
51+
52+
for _ in $(seq 1 120); do
53+
[[ -s "${ENGINES}" ]] && break
54+
sleep 1
55+
done
56+
if [[ ! -s "${ENGINES}" ]]; then
57+
echo " FAIL openwork: the engine never started"
58+
cat "${LOG}"
59+
exit 1
60+
fi
61+
62+
python3 - "${ENGINES}" "${STATE}/workspace" "${MODEL}" "${HOSTED}" <<'PY'
63+
import json, sys, time, urllib.parse, urllib.request
64+
65+
engines, workspace, model, hosted = sys.argv[1:5]
66+
# The file is written before the engine is registered in it.
67+
for _ in range(120):
68+
entries = json.load(open(engines)).get("entries") or []
69+
if entries:
70+
break
71+
time.sleep(1)
72+
assert entries, "the engine never registered"
73+
entry = entries[0]
74+
base, auth = entry["url"], entry["authProbe"]
75+
query = "?directory=" + urllib.parse.quote(workspace)
76+
77+
def call(path, body=None, timeout=30):
78+
data = None if body is None else json.dumps(body).encode()
79+
request = urllib.request.Request(base + path + query, data=data,
80+
headers={"Authorization": auth, "Content-Type": "application/json"})
81+
with urllib.request.urlopen(request, timeout=timeout) as response:
82+
return json.load(response)
83+
84+
for _ in range(60):
85+
try:
86+
providers = call("/config/providers")
87+
break
88+
except OSError:
89+
time.sleep(1)
90+
by_id = {p["id"]: p for p in providers["providers"]}
91+
connected = call("/provider")["connected"]
92+
selected = call("/config").get("model", "")
93+
ours = [pid for pid in ("runanywhere", "runanywhere-local") if pid in by_id]
94+
assert ours, f"no RunAnywhere provider in {sorted(by_id)}"
95+
for pid in ours:
96+
assert pid in connected, f"{pid} is not connected: {connected}"
97+
print(f" ok openwork lists {len(by_id[pid]['models'])} models under {pid}")
98+
assert selected.endswith("/" + model), f"selected {selected!r}, launched {model!r}"
99+
print(f" ok openwork selects {selected}")
100+
101+
targets = [(selected.split("/", 1)[0], model)]
102+
if hosted:
103+
targets.append(("runanywhere", hosted))
104+
for provider, model_id in targets:
105+
session = call("/session", {})["id"]
106+
reply = call(f"/session/{session}/message", {
107+
"model": {"providerID": provider, "modelID": model_id},
108+
"parts": [{"type": "text", "text": "Reply with exactly the word pong and nothing else."}],
109+
}, timeout=300)
110+
error = reply.get("info", {}).get("error")
111+
text = " ".join(p.get("text", "") for p in reply.get("parts", []) if p.get("type") == "text")
112+
assert not error and text.strip().strip(".").lower() == "pong", f"{provider}/{model_id}: error={error} text={text!r}"
113+
print(f" ok openwork answered on {provider}/{model_id}: {text.strip()[:40]!r}")
114+
PY
115+
116+
trap - EXIT
117+
cleanup || exit 1
118+
# The engine is stopped by the app on its way out, which takes a few seconds.
119+
for _ in $(seq 1 20); do
120+
pgrep -f "OpenWork.app/Contents/Resources/sidecars/opencode" >/dev/null || break
121+
sleep 1
122+
done
123+
if pgrep -f "OpenWork.app/Contents/Resources/sidecars/opencode" >/dev/null; then
124+
echo " FAIL openwork: its engine is still running after the app quit"
125+
exit 1
126+
fi
127+
if [[ "${HOME_WORKSPACE_EXISTED}" == 0 && -e "${HOME}/OpenWork Chat" ]]; then
128+
echo " FAIL openwork: the default workspace landed in the real home"
129+
exit 1
130+
fi
131+
echo " ok openwork quit cleanly"

‎scripts/test/e2e.sh‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,4 +174,12 @@ else
174174
echo " skip modalities (WALLY_E2E_MODALITIES=0)"
175175
fi
176176

177+
# The OpenWork desktop app on wally's private profile; opt-in, macOS only.
178+
if bash "${ROOT}/scripts/test/e2e-openwork.sh" "${WALLY}"; then
179+
:
180+
else
181+
echo " FAIL openwork"
182+
fail=1
183+
fi
184+
177185
exit "${fail}"

‎skills/runanywhere/SKILL.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ If opencode is not installed, `wally` says so and prints the install command
5858
(`npm i -g opencode-ai`) rather than failing. Install it, then run the same
5959
line again.
6060

61+
`wally openwork -m <model>` opens the OpenWork desktop app on a private profile,
62+
with your account's hosted models in its model picker and, for a local `-m`, that
63+
local model beside them. On macOS it offers `brew install --cask openwork` when
64+
the app is missing. The app flow has been checked on macOS only.
65+
6166
Offer to explain a harness before running it. Someone who has just signed up
6267
does not yet know what opencode is, and "would you like me to explain how the
6368
opencode harness works?" is a better second message than a launched TUI.

‎src/app.rs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ pub fn configure_app(app: &mut App) {
7575
crate::commands::register_editors(app);
7676
crate::commands::register_codex(app);
7777
crate::commands::register_chatgpt(app);
78+
crate::commands::register_openwork(app);
7879
crate::commands::register_harness(app); // coding agents
7980
crate::commands::register_default_models(app);
8081

@@ -129,6 +130,7 @@ pub fn configure_app(app: &mut App) {
129130
"claude-desktop",
130131
"codex",
131132
"chatgpt",
133+
"openwork",
132134
"hermes",
133135
"openclaw",
134136
"deepseek",
@@ -193,7 +195,7 @@ fn is_passthrough_command(token: &str) -> bool {
193195
/// the help page except `update` (it is the update) and `uninstall` (there is
194196
/// nothing left to update). Help pages show it too, from run().
195197
fn shows_update_notice(path: &[String]) -> bool {
196-
const NAMES: &[&str] = &["run", "serve", "models", "account", "about"];
198+
const NAMES: &[&str] = &["run", "serve", "models", "account", "about", "openwork"];
197199
path.first()
198200
.is_some_and(|name| NAMES.contains(&name.as_str()) || is_passthrough_command(name))
199201
}

‎src/cli_formatter.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,6 +1195,11 @@ mod tests {
11951195
"Open ChatGPT's Codex with a model",
11961196
"Coding tools",
11971197
),
1198+
leaf(
1199+
"openwork",
1200+
"Open OpenWork with a local or cloud model",
1201+
"Coding tools",
1202+
),
11981203
leaf(
11991204
"opencode",
12001205
"Open OpenCode with a local or cloud model",

0 commit comments

Comments
 (0)