openvibers/openvibe.link Commits

Muse Code in Link's window, and its login for Codes jobs (#152)

OP OpenVibers committed 2 days ago · 22bb68d4e1 · 9 files, +133 −30

* Muse Code on this machine: detect it in the window, name its login for jobs

The window's Codes section lists Muse Code like the other agent CLIs:
ready with its version when muse is installed and its plan session sits
in ~/.config/muse/auth.json (nested under providers.meta), the sign-in
command when it is installed but signed out, and the installer when it is
missing (a new install field on the agent, per OS). The installer's
--plan-logins names the Muse login for the job worker's jobs, and
docs/worker.md says how to set it up (install, muse login, plan logins,
contributor mode off on jobs).

* Muse Code: only Meta's session counts as signed in

auth.json holds one entry per provider; a third-party provider key is not the Muse plan a job runs on, so the window now reads providers.meta alone.

Changes

ddiff --git a/docs/worker.md b/docs/worker.md
iindex 2466f67..7d38fed 100644
---- a/docs/worker.md
++++ b/docs/worker.md
@@@ -37,6 +37,13 @@ line, as git's credential store writes them), install any agent CLIs system-wide
openvibe.codes/run then clones the repository inside the job's sandbox, runs the agents there, and hands back the
summary, the changed files and the patch; it never pushes to the branch it started from.
++Muse Code runs in the same sandbox, on the person's own Muse sign-in: install it
++(`curl -fsSL https://dev.meta.ai/install.sh | sh`, then `muse login` with the person's own plan), and name its login
++file (`~/.config/muse/auth.json`) with the installer's `--plan-logins`, so a job sees it read-only while it runs.
++A job runs Muse with contributor mode off: Meta does not train on the run. (A `META_API_KEY` in the runner's `env`
++would bill the run to that key ahead of the plan: leave it unset for plan billing.) Link's window (the Codes section)
++shows whether Muse Code is installed and signed in on this computer, with the command that sets it up.
++
Inside the sandbox nothing of yours is reachable (no home folder, no git sign-in), so a private repository clones only
with a token from this `env`. Codes CLI 0.6.1 and later sends it to git as an `Authorization` header for that host
alone, through `GIT_CONFIG_*` variables: it is never on a command line, in the clone's remote URL or in its
@@@ -411,8 +418,8 @@ never change it), at the same path it has under the owner's home. The copy goes
own directory and disappears when the job ends. Its content is never logged, never uploaded and never part of a result.
The installer's `--plan-logins` does this on Linux: it names the files that are already there
--(`~/.claude/.credentials.json`, `~/.codex/auth.json`, `~/.local/share/opencode/auth.json` and
--`~/.commandcode/auth.json`), skips a tool whose login file is missing or empty with a line giving that tool's own login
++(`~/.claude/.credentials.json`, `~/.codex/auth.json`, `~/.local/share/opencode/auth.json`,
++`~/.commandcode/auth.json` and `~/.config/muse/auth.json`), skips a tool whose login file is missing or empty with a line giving that tool's own login
command, and sets the service's `HOME` to your home. It only looks at whether each file is there: its content is
never read, copied or printed. Sign a tool in with its own login, then run the installer again with `--plan-logins` to
name the new file.
ddiff --git a/install/install.sh b/install/install.sh
iindex e795d71..d1a0f03 100755
---- a/install/install.sh
++++ b/install/install.sh
@@@ -38,7 +38,7 @@
# runs as root, which run_as and the egress need), and the OpenVibe.Codes job runner is added when
# Node.js 20+ is installed system-wide. Linux only. docs/worker.md has the details.
# --plan-logins with --worker (implied): name the coding tools' own login files that are already signed in on this
--# computer (Claude Code, Codex, OpenCode, Command Code) as the worker's plan logins, so a Codes job
++# computer (Claude Code, Codex, OpenCode, Command Code, Muse Code) as the worker's plan logins, so a Codes job
# uses the plan you signed in for. A tool that is not installed or not signed in is skipped with a
# line saying how to sign it in. The installer never reads, copies or prints a login's content, and
# sets the Link service's HOME to your home (the logins must sit under it). Linux only.
@@@ -263,6 +263,7 @@ claude-code|claude|.claude/.credentials.json|claude
codex|codex|.codex/auth.json|codex login
opencode|opencode|.local/share/opencode/auth.json|opencode auth login
command-code|cmd|.commandcode/auth.json|cmd login
++muse|muse|.config/muse/auth.json|muse login
EOF
}
@@@ -1173,16 +1174,17 @@ if [ "$WORKER" = 1 ]; then
say "plan logins: $_named named for your jobs; each job sees them read-only, only while it runs"
else
say "plan logins: none named yet: sign in to the coding tools you use with their own login, then run this installer"
-- say " again with --plan-logins (Claude Code: claude; Codex: codex login; OpenCode: opencode auth login; Command Code: cmd login)"
++ say " again with --plan-logins (Claude Code: claude; Codex: codex login; OpenCode: opencode auth login; Command Code: cmd login; Muse Code: muse login)"
fi
-- if [ "$_named" -lt 4 ]; then
++ if [ "$_named" -lt 5 ]; then
say " the line above each skipped tool has its own login command; run it, then run this installer again with --plan-logins"
fi
fi
say "the job worker is on: jobs run as openvibe-job in the worker's sandbox, with public egress"
say "add your agent keys (DEEPSEEK_API_KEY, ANTHROPIC_API_KEY, OPENAI_API_KEY, and GITHUB_TOKEN to push branches) to"
say " worker.functions[openvibe-codes].env in $CONF/config.json, and install agent CLIs system-wide"
-- say " (sudo npm install -g @anthropic-ai/claude-code @openai/codex), then: sudo systemctl restart openvibe-link"
++ say " (sudo npm install -g @anthropic-ai/claude-code @openai/codex; Muse Code: curl -fsSL https://dev.meta.ai/install.sh | sh,"
++ say " then muse login), then: sudo systemctl restart openvibe-link"
fi
# ---- the Adeept kit's stock server: never run it (fixed admin:123456 login on 0.0.0.0:8888, MJPEG on :5000) ----
ddiff --git a/internal/agent/panel_codes.go b/internal/agent/panel_codes.go
iindex 32fff02..54b673f 100644
---- a/internal/agent/panel_codes.go
++++ b/internal/agent/panel_codes.go
@@@ -16,6 +16,7 @@ import (
"os/exec"
"path/filepath"
"regexp"
++ "runtime"
"sort"
"strings"
"sync"
@@@ -44,24 +45,31 @@ const (
// codesAgentSpec is one coding-agent CLI on the machine, and how OpenVibe.Codes finds it: the binary on PATH, the
// login file its CLI writes, and the command that signs it in. A CLI with no login file (dsh reads its provider
--// settings from the machine) is ready once its binary is there.
++// settings from the machine) is ready once its binary is there. Install is the command that installs the CLI, shown
++// when it is missing (InstallWindows on Windows); empty when Link knows no installer for it.
type codesAgentSpec struct {
-- ID string
-- Name string
-- Bin string
-- Login string
-- Hint string
++ ID string
++ Name string
++ Bin string
++ Login string
++ Hint string
++ Install string
++ InstallWindows string
}
// codesAgentSpecs are the CLIs the desktop's Codes module drives (domains plan §13.1).
var codesAgentSpecs = []codesAgentSpec{
-- {"claude-code", "Claude Code", "claude", ".claude/.credentials.json", "claude auth login"},
-- {"codex", "Codex", "codex", ".codex/auth.json", "codex login"},
-- {"opencode", "OpenCode", "opencode", ".local/share/opencode/auth.json", "opencode auth login"},
++ {ID: "claude-code", Name: "Claude Code", Bin: "claude", Login: ".claude/.credentials.json", Hint: "claude auth login"},
++ {ID: "codex", Name: "Codex", Bin: "codex", Login: ".codex/auth.json", Hint: "codex login"},
++ {ID: "opencode", Name: "OpenCode", Bin: "opencode", Login: ".local/share/opencode/auth.json", Hint: "opencode auth login"},
// Gemini CLI signs in from inside the CLI with /auth; `gemini` is the command that starts it.
-- {"gemini", "Gemini CLI", "gemini", ".gemini/oauth_creds.json", "gemini"},
++ {ID: "gemini", Name: "Gemini CLI", Bin: "gemini", Login: ".gemini/oauth_creds.json", Hint: "gemini"},
// dsh keeps its model settings in its profile, not in a plan login: installed is as much as is known here.
-- {"dsh", "DeepSeek Harness", "dsh", "", ""},
++ {ID: "dsh", Name: "DeepSeek Harness", Bin: "dsh"},
++ // Muse Code keeps the person's own plan session in auth.json under providers.meta (Meta's installer and login;
++ // Meta's quickstart, checked 2026-11-10). A Codes or Actor job on this machine runs it with contributor mode off.
++ {ID: "muse", Name: "Muse Code", Bin: "muse", Login: ".config/muse/auth.json", Hint: "muse login",
++ Install: "curl -fsSL https://dev.meta.ai/install.sh | sh", InstallWindows: "irm https://dev.meta.ai/install.ps1 | iex"},
}
// codesProbe is what detection needs from the machine, so a test can fake it.
@@@ -105,6 +113,7 @@ func detectCodesAgents(p codesProbe) []panel.CodesAgent {
}
}
if bin == "" {
++ a.Install = codesInstall(spec)
out = append(out, a)
continue
}
@@@ -143,9 +152,24 @@ func codesSignedIn(raw []byte) bool {
if nested, ok := login["claudeAiOauth"].(map[string]any); ok && usableSession(nested) {
return true
}
++ // Muse Code nests one session per provider: { "providers": { "meta": { "access_token": … } } }. The plan that
++ // runs a job is Meta's, so a third-party provider's key does not count as signed in.
++ if providers, ok := login["providers"].(map[string]any); ok {
++ if meta, ok := providers["meta"].(map[string]any); ok && usableSession(meta) {
++ return true
++ }
++ }
return false
}
++// codesInstall is the command that installs a missing CLI on this OS, or empty when Link knows none.
++func codesInstall(spec codesAgentSpec) string {
++ if runtime.GOOS == "windows" {
++ return spec.InstallWindows
++ }
++ return spec.Install
++}
++
func usableSession(login map[string]any) bool {
for _, key := range []string{"access_token", "accessToken", "api_key", "apiKey", "refresh_token", "refreshToken", "token", "tokens", "account"} {
switch v := login[key].(type) {
@@@ -205,7 +229,7 @@ func openCodeAuth(bin string) (signedIn, known bool) {
return false, true
}
--// codesAgentsCache keeps the last probe, so a window polled every three seconds does not run five binaries each time.
++// codesAgentsCache keeps the last probe, so a window polled every three seconds does not run every binary each time.
type codesAgentsCache struct {
mu sync.Mutex
at time.Time
ddiff --git a/internal/agent/panel_codes_test.go b/internal/agent/panel_codes_test.go
iindex 55c2885..8dd8664 100644
---- a/internal/agent/panel_codes_test.go
++++ b/internal/agent/panel_codes_test.go
@@@ -67,6 +67,63 @@ func TestCodesLoginFileWithoutASessionIsSignIn(t *testing.T) {
!codesSignedIn([]byte(`{"claudeAiOauth":{"accessToken":"x"}}`)) {
t.Fatal("a usable login was not recognised")
}
++ // Muse Code nests its plan session under providers.meta; an empty providers map is no session.
++ if !codesSignedIn([]byte(`{"schema_version":1,"providers":{"meta":{"access_token":"not-a-real-token","subs_active":true}}}`)) {
++ t.Fatal("a Muse login was not recognised")
++ }
++ if codesSignedIn([]byte(`{"schema_version":1,"providers":{"meta":{}}}`)) || codesSignedIn([]byte(`{"schema_version":1,"providers":{}}`)) {
++ t.Fatal("an empty Muse providers map was taken as signed in")
++ }
++ if codesSignedIn([]byte(`{"schema_version":1,"providers":{"openai":{"api_key":"sk-not-meta"}}}`)) {
++ t.Fatal("a third-party provider's key was taken as a Muse plan sign-in")
++ }
++}
++
++// Muse Code in the window: ready with its version when its binary and login are there, the login command when it is
++// installed but signed out, and the installer when it is missing.
++func TestDetectCodesAgentsSeesMuseCode(t *testing.T) {
++ home := t.TempDir()
++ login := filepath.Join(home, ".config", "muse", "auth.json")
++ if err := os.MkdirAll(filepath.Dir(login), 0o700); err != nil {
++ t.Fatal(err)
++ }
++ if err := os.WriteFile(login, []byte(`{"schema_version":1,"providers":{"meta":{"access_token":"not-a-real-token"}}}`), 0o600); err != nil {
++ t.Fatal(err)
++ }
++ probe := codesProbe{
++ Home: home,
++ LookPath: func(bin string) (string, error) { return "/usr/bin/" + bin, nil },
++ ReadFile: os.ReadFile,
++ Version: func(string) string { return "1.4.15" },
++ }
++ got := map[string]panel.CodesAgent{}
++ for _, a := range detectCodesAgents(probe) {
++ got[a.ID] = a
++ }
++ if a := got["muse"]; a.Name != "Muse Code" || a.State != "ready" || a.Version != "1.4.15" || a.Hint != "" || a.Install != "" {
++ t.Fatalf("muse ready: %+v", a)
++ }
++ if err := os.WriteFile(login, []byte(`{"schema_version":1}`), 0o600); err != nil {
++ t.Fatal(err)
++ }
++ for _, a := range detectCodesAgents(probe) {
++ got[a.ID] = a
++ }
++ if a := got["muse"]; a.State != "signin" || a.Hint != "muse login" || a.Install != "" {
++ t.Fatalf("muse signin: %+v", a)
++ }
++ probe.LookPath = func(bin string) (string, error) {
++ if bin == "muse" {
++ return "", errors.New("not on PATH")
++ }
++ return "/usr/bin/" + bin, nil
++ }
++ for _, a := range detectCodesAgents(probe) {
++ got[a.ID] = a
++ }
++ if a := got["muse"]; a.State != "missing" || a.Hint != "" || !strings.Contains(a.Install, "dev.meta.ai") {
++ t.Fatalf("muse missing: %+v", a)
++ }
}
func TestCodesFolderUsesTildeAndTheTitleIsTheFirstLine(t *testing.T) {
ddiff --git a/internal/panel/panel.go b/internal/panel/panel.go
iindex 133b7d4..1a1c036 100644
---- a/internal/panel/panel.go
++++ b/internal/panel/panel.go
@@@ -105,13 +105,15 @@ type Activity struct {
Text string `json:"text"`
}
--// CodesAgent is one coding-agent CLI on this computer: ready, needs signing in (Hint is the command), or missing.
++// CodesAgent is one coding-agent CLI on this computer: ready, needs signing in (Hint is the command), or missing
++// (Install is the command that installs it, when Link knows one).
type CodesAgent struct {
ID string `json:"id"`
Name string `json:"name"`
State string `json:"state"`
Version string `json:"version,omitempty"`
Hint string `json:"hint,omitempty"`
++ Install string `json:"install,omitempty"`
}
// CodesTask is one Codes task that runs, or ran, on this computer.
ddiff --git a/internal/panel/ui/README.md b/internal/panel/ui/README.md
iindex 52ecf26..935109d 100644
---- a/internal/panel/ui/README.md
++++ b/internal/panel/ui/README.md
@@@ -57,7 +57,8 @@ Every key is optional. A section with nothing in it is left out; the window neve
- `machine.id` is the machine's own id after pairing, so "New task on this computer" can preselect it.
- `running[].kind` is `agent`, `desk`, `share` or `model`.
--- `codes.agents[].state` is `ready`, `signin` (then `hint` is the CLI's own sign-in command) or `missing`.
++- `codes.agents[].state` is `ready`, `signin` (then `hint` is the CLI's own sign-in command) or `missing`
++ (then `install` is the command that installs it, when Link knows one). The window shows the command to copy.
- `codes.tasks[]` are the Codes jobs this worker ran today: `state` is `running`, `waiting`, `done`, `failed` or
`stopped`; `folder` shows the home directory as `~`; `url` is only ever `https://openvibe.codes/tasks/<id>`.
- `machine.needs_you` counts what waits for the person: it puts a dot on the ring.
ddiff --git a/internal/panel/ui/panel.js b/internal/panel/ui/panel.js
iindex 14f021f..3ac5a1f 100644
---- a/internal/panel/ui/panel.js
++++ b/internal/panel/ui/panel.js
@@@ -32,10 +32,11 @@
$('[data-lk-agents]').replaceChildren.apply($('[data-lk-agents]'), agents.map(function (a) {
var li = el('li', 'lk-agent'); li.setAttribute('data-state', a.state || 'missing');
li.append(document.createTextNode(a.name || a.id), el('small', '', AGENT_STATE[a.state] || a.state || ''));
-- if (a.state === 'signin' && a.hint) {
-- var code = el('code', '', a.hint); var copy = el('button', '', 'Copy'); copy.type = 'button';
-- copy.setAttribute('aria-label', 'Copy ' + a.hint);
-- copy.addEventListener('click', function () { if (navigator.clipboard) navigator.clipboard.writeText(a.hint).then(function () { copy.textContent = 'Copied'; }); });
++ var setup = (a.state === 'signin' && a.hint) || (a.state === 'missing' && a.install) || '';
++ if (setup) {
++ var code = el('code', '', setup); var copy = el('button', '', 'Copy'); copy.type = 'button';
++ copy.setAttribute('aria-label', 'Copy ' + setup);
++ copy.addEventListener('click', function () { if (navigator.clipboard) navigator.clipboard.writeText(setup).then(function () { copy.textContent = 'Copied'; }); });
li.append(code, copy);
}
return li;
ddiff --git a/internal/panel/ui/testdata/state.json b/internal/panel/ui/testdata/state.json
iindex 052a4fc..cdba604 100644
---- a/internal/panel/ui/testdata/state.json
++++ b/internal/panel/ui/testdata/state.json
@@@ -130,6 +130,12 @@
"name": "DeepSeek Harness",
"state": "ready",
"version": "0.2.1"
++ },
++ {
++ "id": "muse",
++ "name": "Muse Code",
++ "state": "missing",
++ "install": "curl -fsSL https://dev.meta.ai/install.sh | sh"
}
],
"tasks": [
ddiff --git a/scripts/test-install-plan-logins.sh b/scripts/test-install-plan-logins.sh
iindex 0a73fae..8af7142 100755
---- a/scripts/test-install-plan-logins.sh
++++ b/scripts/test-install-plan-logins.sh
@@@ -15,17 +15,18 @@ home="$tmp/home"
bin="$tmp/bin"
# A clean PATH: the fake tools, then the system's own. The invoking user's PATH may have the real Command Code on it.
path="$bin:/usr/bin:/bin"
--mkdir -p "$home/.claude" "$home/.codex" "$home/.local/share/opencode" "$bin"
++mkdir -p "$home/.claude" "$home/.codex" "$home/.local/share/opencode" "$home/.config/muse" "$bin"
# The installer needs python3; a clean PATH still has to find it (it may live in /usr/local/bin).
ln -s "$(command -v python3)" "$bin/python3"
--for tool in claude codex opencode; do
++for tool in claude codex opencode muse; do
printf '#!/bin/sh\nexit 0\n' > "$bin/$tool"
chmod +x "$bin/$tool"
done
--# Claude Code and OpenCode are signed in; Codex is installed but its login file is empty; Command Code is not installed.
++# Claude Code, OpenCode and Muse Code are signed in; Codex is installed but its login file is empty; Command Code is not installed.
printf '%s\n' '{"access_token":"CLAUDE-LOGIN-SECRET"}' > "$home/.claude/.credentials.json"
: > "$home/.codex/auth.json"
printf '%s\n' '{"token":"OPENCODE-LOGIN-SECRET"}' > "$home/.local/share/opencode/auth.json"
++printf '%s\n' '{"providers":{"meta":{"access_token":"MUSE-LOGIN-SECRET"}}}' > "$home/.config/muse/auth.json"
dry="$(HOME="$home" PATH="$path" OPENVIBE_INSTALL_PLAN_LOGINS_DRY_RUN=1 sh "$installer" --plan-logins 2>&1)" ||
fail "the dry run exited non-zero"
@@@ -36,9 +37,10 @@ check '3. name the coding tools'
check '4. write the names'
check 'claude-code: signed in (~/.claude/.credentials.json): named for your jobs'
check 'opencode: signed in (~/.local/share/opencode/auth.json): named for your jobs'
++check 'muse: signed in (~/.config/muse/auth.json): named for your jobs'
check "codex: installed but not signed in: skipped (run \`codex login\`"
check "command-code: not installed: skipped (install it and sign in with \`cmd login\`"
--for secret in CLAUDE-LOGIN-SECRET OPENCODE-LOGIN-SECRET; do
++for secret in CLAUDE-LOGIN-SECRET OPENCODE-LOGIN-SECRET MUSE-LOGIN-SECRET; do
printf '%s\n' "$dry" | grep -F -- "$secret" >/dev/null && fail "the dry run printed a login's content"
done
@@@ -57,7 +59,7 @@ existing="$home/.codex/old.json"
printf '{"server": "https://openvibe.bot", "plugins": [{"name": "dryrun"}], "worker": {"enabled": true, "plan_logins": ["%s"]}}\n' "$existing" > "$conf"
written="$(HOME="$home" PATH="$path" OPENVIBE_INSTALL_PLAN_LOGINS_CONFIG_ONLY="$conf" sh "$installer" --plan-logins 2>&1)" ||
fail "writing worker.plan_logins exited non-zero"
--for secret in CLAUDE-LOGIN-SECRET OPENCODE-LOGIN-SECRET; do
++for secret in CLAUDE-LOGIN-SECRET OPENCODE-LOGIN-SECRET MUSE-LOGIN-SECRET; do
printf '%s\n' "$written" | grep -F -- "$secret" >/dev/null && fail "the config run printed a login's content"
grep -F -- "$secret" "$conf" >/dev/null && fail "a login's content reached config.json"
done
@@@ -70,7 +72,8 @@ assert c['server'] == 'https://openvibe.bot' and c['plugins'] == [{'name': 'dryr
assert w['enabled'] is True, 'the worker section stays'
want = sorted({os.path.join(home, '.codex', 'old.json'),
os.path.join(home, '.claude', '.credentials.json'),
-- os.path.join(home, '.local', 'share', 'opencode', 'auth.json')})
++ os.path.join(home, '.local', 'share', 'opencode', 'auth.json'),
++ os.path.join(home, '.config', 'muse', 'auth.json')})
assert w['plan_logins'] == want, w['plan_logins']
assert stat.S_IMODE(os.stat(path).st_mode) == 0o600, 'the config stays private'
PY