openvibers/openvibe.link Commits
Muse Code in Link's window, and its login for Codes jobs (#152)
Changes
@@@ -37,6 +37,13 @@ line, as git's credential store writes them), install any agent CLIs system-wideopenvibe.codes/run then clones the repository inside the job's sandbox, runs the agents there, and hands back thesummary, 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 onlywith a token from this `env`. Codes CLI 0.6.1 and later sends it to git as an `Authorization` header for that hostalone, 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 goesown 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 logincommand, and sets the service's `HOME` to your home. It only looks at whether each file is there: its content isnever read, copied or printed. Sign a tool in with its own login, then run the installer again with `--plan-logins` toname the new file.@@@ -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|claudecodex|codex|.codex/auth.json|codex loginopencode|opencode|.local/share/opencode/auth.json|opencode auth logincommand-code|cmd|.commandcode/auth.json|cmd login++muse|muse|.config/muse/auth.json|muse loginEOF}@@@ -1173,16 +1174,17 @@ if [ "$WORKER" = 1 ]; thensay "plan logins: $_named named for your jobs; each job sees them read-only, only while it runs"elsesay "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 ]; thensay " the line above each skipped tool has its own login command; run it, then run this installer again with --plan-logins"fifisay "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) ----@@@ -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.Mutexat time.Time@@@ -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) {@@@ -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.@@@ -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.@@@ -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;@@@ -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": [@@@ -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; doprintf '#!/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; doprintf '%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; doprintf '%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': 'dryrassert 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
@@ -37,6 +37,13 @@ line, as git's credential store writes them), install any agent CLIs system-wide
@@ -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
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.
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
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
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
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
@@ -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.
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
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
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
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.
name the new file.
@@ -38,7 +38,7 @@
@@ -38,7 +38,7 @@
# runs as root, which run_as and the egress need), and the OpenVibe.Codes job runner is added when
# 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.
# 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
# --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
# 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
# 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.
# 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
@@ -263,6 +263,7 @@ claude-code|claude|.claude/.credentials.json|claude
codex|codex|.codex/auth.json|codex login
codex|codex|.codex/auth.json|codex login
opencode|opencode|.local/share/opencode/auth.json|opencode auth login
opencode|opencode|.local/share/opencode/auth.json|opencode auth login
command-code|cmd|.commandcode/auth.json|cmd login
command-code|cmd|.commandcode/auth.json|cmd login
+muse|muse|.config/muse/auth.json|muse login
EOF
EOF
}
}
@@ -1173,16 +1174,17 @@ if [ "$WORKER" = 1 ]; then
@@ -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"
say "plan logins: $_named named for your jobs; each job sees them read-only, only while it runs"
else
else
say "plan logins: none named yet: sign in to the coding tools you use with their own login, then run this installer"
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
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"
say " the line above each skipped tool has its own login command; run it, then run this installer again with --plan-logins"
fi
fi
fi
fi
say "the job worker is on: jobs run as openvibe-job in the worker's sandbox, with public egress"
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 "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 " 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
fi
# ---- the Adeept kit's stock server: never run it (fixed admin:123456 login on 0.0.0.0:8888, MJPEG on :5000) ----
# ---- the Adeept kit's stock server: never run it (fixed admin:123456 login on 0.0.0.0:8888, MJPEG on :5000) ----
@@ -16,6 +16,7 @@ import (
@@ -16,6 +16,7 @@ import (
"os/exec"
"os/exec"
"path/filepath"
"path/filepath"
"regexp"
"regexp"
+ "runtime"
"sort"
"sort"
"strings"
"strings"
"sync"
"sync"
@@ -44,24 +45,31 @@ const (
@@ -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
// 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
// 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 {
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).
// codesAgentSpecs are the CLIs the desktop's Codes module drives (domains plan §13.1).
var codesAgentSpecs = []codesAgentSpec{
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 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 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.
// codesProbe is what detection needs from the machine, so a test can fake it.
@@ -105,6 +113,7 @@ func detectCodesAgents(p codesProbe) []panel.CodesAgent {
@@ -105,6 +113,7 @@ func detectCodesAgents(p codesProbe) []panel.CodesAgent {
}
}
}
}
if bin == "" {
if bin == "" {
+ a.Install = codesInstall(spec)
out = append(out, a)
out = append(out, a)
continue
continue
}
}
@@ -143,9 +152,24 @@ func codesSignedIn(raw []byte) bool {
@@ -143,9 +152,24 @@ func codesSignedIn(raw []byte) bool {
if nested, ok := login["claudeAiOauth"].(map[string]any); ok && usableSession(nested) {
if nested, ok := login["claudeAiOauth"].(map[string]any); ok && usableSession(nested) {
return true
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
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 {
func usableSession(login map[string]any) bool {
for _, key := range []string{"access_token", "accessToken", "api_key", "apiKey", "refresh_token", "refreshToken", "token", "tokens", "account"} {
for _, key := range []string{"access_token", "accessToken", "api_key", "apiKey", "refresh_token", "refreshToken", "token", "tokens", "account"} {
switch v := login[key].(type) {
switch v := login[key].(type) {
@@ -205,7 +229,7 @@ func openCodeAuth(bin string) (signedIn, known bool) {
@@ -205,7 +229,7 @@ func openCodeAuth(bin string) (signedIn, known bool) {
return false, true
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 {
type codesAgentsCache struct {
mu sync.Mutex
mu sync.Mutex
at time.Time
at time.Time
@@ -67,6 +67,63 @@ func TestCodesLoginFileWithoutASessionIsSignIn(t *testing.T) {
@@ -67,6 +67,63 @@ func TestCodesLoginFileWithoutASessionIsSignIn(t *testing.T) {
!codesSignedIn([]byte(`{"claudeAiOauth":{"accessToken":"x"}}`)) {
!codesSignedIn([]byte(`{"claudeAiOauth":{"accessToken":"x"}}`)) {
t.Fatal("a usable login was not recognised")
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) {
func TestCodesFolderUsesTildeAndTheTitleIsTheFirstLine(t *testing.T) {
@@ -105,13 +105,15 @@ type Activity struct {
@@ -105,13 +105,15 @@ type Activity struct {
Text string `json:"text"`
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 {
type CodesAgent struct {
ID string `json:"id"`
ID string `json:"id"`
Name string `json:"name"`
Name string `json:"name"`
State string `json:"state"`
State string `json:"state"`
Version string `json:"version,omitempty"`
Version string `json:"version,omitempty"`
Hint string `json:"hint,omitempty"`
Hint string `json:"hint,omitempty"`
+ Install string `json:"install,omitempty"`
}
}
// CodesTask is one Codes task that runs, or ran, on this computer.
// CodesTask is one Codes task that runs, or ran, on this computer.
@@ -57,7 +57,8 @@ Every key is optional. A section with nothing in it is left out; the window neve
@@ -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.
- `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`.
- `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
- `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>`.
`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.
- `machine.needs_you` counts what waits for the person: it puts a dot on the ring.
@@ -32,10 +32,11 @@
@@ -32,10 +32,11 @@
$('[data-lk-agents]').replaceChildren.apply($('[data-lk-agents]'), agents.map(function (a) {
$('[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');
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 || ''));
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);
li.append(code, copy);
}
}
return li;
return li;
@@ -130,6 +130,12 @@
@@ -130,6 +130,12 @@
"name": "DeepSeek Harness",
"name": "DeepSeek Harness",
"state": "ready",
"state": "ready",
"version": "0.2.1"
"version": "0.2.1"
+ },
+ {
+ "id": "muse",
+ "name": "Muse Code",
+ "state": "missing",
+ "install": "curl -fsSL https://dev.meta.ai/install.sh | sh"
}
}
],
],
"tasks": [
"tasks": [
@@ -15,17 +15,18 @@ home="$tmp/home"
@@ -15,17 +15,18 @@ home="$tmp/home"
bin="$tmp/bin"
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.
# 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"
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).
# 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"
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"
printf '#!/bin/sh\nexit 0\n' > "$bin/$tool"
chmod +x "$bin/$tool"
chmod +x "$bin/$tool"
done
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"
printf '%s\n' '{"access_token":"CLAUDE-LOGIN-SECRET"}' > "$home/.claude/.credentials.json"
: > "$home/.codex/auth.json"
: > "$home/.codex/auth.json"
printf '%s\n' '{"token":"OPENCODE-LOGIN-SECRET"}' > "$home/.local/share/opencode/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)" ||
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"
fail "the dry run exited non-zero"
@@ -36,9 +37,10 @@ check '3. name the coding tools'
@@ -36,9 +37,10 @@ check '3. name the coding tools'
check '4. write the names'
check '4. write the names'
check 'claude-code: signed in (~/.claude/.credentials.json): named for your jobs'
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 '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 "codex: installed but not signed in: skipped (run \`codex login\`"
check "command-code: not installed: skipped (install it and sign in with \`cmd 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"
printf '%s\n' "$dry" | grep -F -- "$secret" >/dev/null && fail "the dry run printed a login's content"
done
done
@@ -57,7 +59,7 @@ existing="$home/.codex/old.json"
@@ -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"
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)" ||
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"
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"
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"
grep -F -- "$secret" "$conf" >/dev/null && fail "a login's content reached config.json"
done
done
@@ -70,7 +72,8 @@ assert c['server'] == 'https://openvibe.bot' and c['plugins'] == [{'name': 'dryr
@@ -70,7 +72,8 @@ assert c['server'] == 'https://openvibe.bot' and c['plugins'] == [{'name': 'dryr
assert w['enabled'] is True, 'the worker section stays'
assert w['enabled'] is True, 'the worker section stays'
want = sorted({os.path.join(home, '.codex', 'old.json'),
want = sorted({os.path.join(home, '.codex', 'old.json'),
os.path.join(home, '.claude', '.credentials.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 w['plan_logins'] == want, w['plan_logins']
assert stat.S_IMODE(os.stat(path).st_mode) == 0o600, 'the config stays private'
assert stat.S_IMODE(os.stat(path).st_mode) == 0o600, 'the config stays private'
PY
PY