openvibers/openvibe.link Commits
v0.37.1: a pairing in another --home leaves the node service alone (#98)
Changes
@@@ -5,7 +5,7 @@"wave": "T15","plan": "Plan track T15 (Bot complete) and ADR-043 (Bot: devices, pairing, control and safety); the computer plugin and the job worker serve OpenVibe.Actor and the Codes job runner (tracks T14/T17)","kind": "client",-- "version": "0.37.0",++ "version": "0.37.1","release": "v0.36.0: the ai plugin (0.2.0) passes tools to the machine's own model server and hands back the calls the model asked for, never running one, so an agent on OpenVibe (Actor) can work on a model this machine serves; its capabilities say tools: true (contracts 0.277.0). v0.35.0: the Node applies the roles the network's machines hub gives it at each check-in (codes.worker starts the job worker it was installed with, desks.host and ai.host are checked against what the machine was installed for, an unknown role is ignored, and the local configuration always wins), and its status reports them back; a desk's first browser start in a cold sandbox gets 90 s. v0.34.0: on Windows a killed plugin takes every process it started with it (a Job Object with kill-on-close), as the process group does on Linux and macOS, so a bridge helper cannot keep a robot's link open after the Node stops or restarts its plugin; the Node reads what the coding tools remember (read-only, local, redacted) and `openvibe-node memory` uploads it to your own store; the heartbeat carries the machine's fleet of coding agents; the Codes job runner pin is v0.5.2","runtime": "go 1.24 (one static binary per platform); plugins in Python 3; an ESP32 Arduino library","database": "none",@@@ -353,13 +353,28 @@ func finishPair(g *globals, a pairArgs, cfg *config.Config, noConfig bool, stdou}chownLikeDir(g.paths.ConfigFile(), g.paths.ConfigDir)}-- if service.Status(serviceOptions(g, "")) == "running" {-- _ = service.Control(serviceOptions(g, ""), "restart")++ // The service has one name on a machine (service.Name) whatever --home it runs, so a pairing in another --home++ // (a second node, a test node) must not restart it: that would restart a different node (11-02: a test node++ // paired in its own folder beside a working robot's service). Only the default home's pairing restarts it.++ if g.home != "" {++ if serviceStatus(serviceOptions(g, "")) == "running" {++ fmt.Fprintln(stdout, "A node service is running on this machine; it was left alone. If it runs this --home, restart it (Windows: `sc stop openvibe-node` then `sc start openvibe-node`; Linux: `sudo systemctl restart openvibe-node`).")++ }++ return nil++ }++ if serviceStatus(serviceOptions(g, "")) == "running" {++ _ = serviceControl(serviceOptions(g, ""), "restart")fmt.Fprintln(stdout, "The service was running and has been restarted with the new credential.")}return nil}++// The service manager, behind seams so the pairing tests can watch it (service.Status and service.Control otherwise).++var (++ serviceStatus = service.Status++ serviceControl = service.Control++)++func cmdPair(g *globals, a pairArgs, stdout io.Writer) error {code, err := link.NormalizeCode(a.code)if err != nil {@@@ -18,6 +18,7 @@ import ("github.com/OpenVibers/OpenVibe.Node/internal/fakebot""github.com/OpenVibers/OpenVibe.Node/internal/protocol""github.com/OpenVibers/OpenVibe.Node/internal/safety"++ "github.com/OpenVibers/OpenVibe.Node/internal/service""github.com/OpenVibers/OpenVibe.Node/internal/video")@@@ -302,3 +303,28 @@ func TestRunDryRun(t *testing.T) {t.Fatalf("no shutdown stop in logs:\n%s", logs.String())}}++++// A pairing in its own --home never restarts the machine's node service: the service has one name whatever home it runs,++// so it may be another node's (11-02: a test node paired in its own folder beside a working robot's service).++func TestPairInAnotherHomeLeavesTheServiceAlone(t *testing.T) {++ srv := fakebot.New()++ defer srv.Close()++ srv.AddCode("WXYZ2345")++ home := shortHome(t)++ t.Setenv("PYTHONPATH", pythonPath(t))++ var controls []string++ oldStatus, oldControl := serviceStatus, serviceControl++ serviceStatus = func(service.Options) string { return "running" }++ serviceControl = func(_ service.Options, action string) error { controls = append(controls, action); return nil }++ defer func() { serviceStatus, serviceControl = oldStatus, oldControl }()++ out, errOut, code := cli(t, "pair", "--code", "wxyz-2345", "--robot", fakebot.RobotID, "--home", home, "--server", srv.URL())++ if code != 0 {++ t.Fatalf("pair failed: %s", errOut)++ }++ if len(controls) != 0 {++ t.Fatalf("the running service was touched: %v", controls)++ }++ if !strings.Contains(out, "left alone") || strings.Contains(out, "has been restarted") {++ t.Fatalf("the output does not say the service was left alone: %s", out)++ }++}
@@ -5,7 +5,7 @@
@@ -5,7 +5,7 @@
"wave": "T15",
"wave": "T15",
"plan": "Plan track T15 (Bot complete) and ADR-043 (Bot: devices, pairing, control and safety); the computer plugin and the job worker serve OpenVibe.Actor and the Codes job runner (tracks T14/T17)",
"plan": "Plan track T15 (Bot complete) and ADR-043 (Bot: devices, pairing, control and safety); the computer plugin and the job worker serve OpenVibe.Actor and the Codes job runner (tracks T14/T17)",
"kind": "client",
"kind": "client",
- "version": "0.37.0",
+ "version": "0.37.1",
"release": "v0.36.0: the ai plugin (0.2.0) passes tools to the machine's own model server and hands back the calls the model asked for, never running one, so an agent on OpenVibe (Actor) can work on a model this machine serves; its capabilities say tools: true (contracts 0.277.0). v0.35.0: the Node applies the roles the network's machines hub gives it at each check-in (codes.worker starts the job worker it was installed with, desks.host and ai.host are checked against what the machine was installed for, an unknown role is ignored, and the local configuration always wins), and its status reports them back; a desk's first browser start in a cold sandbox gets 90 s. v0.34.0: on Windows a killed plugin takes every process it started with it (a Job Object with kill-on-close), as the process group does on Linux and macOS, so a bridge helper cannot keep a robot's link open after the Node stops or restarts its plugin; the Node reads what the coding tools remember (read-only, local, redacted) and `openvibe-node memory` uploads it to your own store; the heartbeat carries the machine's fleet of coding agents; the Codes job runner pin is v0.5.2",
"release": "v0.36.0: the ai plugin (0.2.0) passes tools to the machine's own model server and hands back the calls the model asked for, never running one, so an agent on OpenVibe (Actor) can work on a model this machine serves; its capabilities say tools: true (contracts 0.277.0). v0.35.0: the Node applies the roles the network's machines hub gives it at each check-in (codes.worker starts the job worker it was installed with, desks.host and ai.host are checked against what the machine was installed for, an unknown role is ignored, and the local configuration always wins), and its status reports them back; a desk's first browser start in a cold sandbox gets 90 s. v0.34.0: on Windows a killed plugin takes every process it started with it (a Job Object with kill-on-close), as the process group does on Linux and macOS, so a bridge helper cannot keep a robot's link open after the Node stops or restarts its plugin; the Node reads what the coding tools remember (read-only, local, redacted) and `openvibe-node memory` uploads it to your own store; the heartbeat carries the machine's fleet of coding agents; the Codes job runner pin is v0.5.2",
"runtime": "go 1.24 (one static binary per platform); plugins in Python 3; an ESP32 Arduino library",
"runtime": "go 1.24 (one static binary per platform); plugins in Python 3; an ESP32 Arduino library",
"database": "none",
"database": "none",
@@ -353,13 +353,28 @@ func finishPair(g *globals, a pairArgs, cfg *config.Config, noConfig bool, stdou
@@ -353,13 +353,28 @@ func finishPair(g *globals, a pairArgs, cfg *config.Config, noConfig bool, stdou
}
}
chownLikeDir(g.paths.ConfigFile(), g.paths.ConfigDir)
chownLikeDir(g.paths.ConfigFile(), g.paths.ConfigDir)
}
}
- if service.Status(serviceOptions(g, "")) == "running" {
- _ = service.Control(serviceOptions(g, ""), "restart")
+ // The service has one name on a machine (service.Name) whatever --home it runs, so a pairing in another --home
+ // (a second node, a test node) must not restart it: that would restart a different node (11-02: a test node
+ // paired in its own folder beside a working robot's service). Only the default home's pairing restarts it.
+ if g.home != "" {
+ if serviceStatus(serviceOptions(g, "")) == "running" {
+ fmt.Fprintln(stdout, "A node service is running on this machine; it was left alone. If it runs this --home, restart it (Windows: `sc stop openvibe-node` then `sc start openvibe-node`; Linux: `sudo systemctl restart openvibe-node`).")
+ }
+ return nil
+ }
+ if serviceStatus(serviceOptions(g, "")) == "running" {
+ _ = serviceControl(serviceOptions(g, ""), "restart")
fmt.Fprintln(stdout, "The service was running and has been restarted with the new credential.")
fmt.Fprintln(stdout, "The service was running and has been restarted with the new credential.")
}
}
return nil
return nil
}
}
+// The service manager, behind seams so the pairing tests can watch it (service.Status and service.Control otherwise).
+var (
+ serviceStatus = service.Status
+ serviceControl = service.Control
+)
+
func cmdPair(g *globals, a pairArgs, stdout io.Writer) error {
func cmdPair(g *globals, a pairArgs, stdout io.Writer) error {
code, err := link.NormalizeCode(a.code)
code, err := link.NormalizeCode(a.code)
if err != nil {
if err != nil {
@@ -18,6 +18,7 @@ import (
@@ -18,6 +18,7 @@ import (
"github.com/OpenVibers/OpenVibe.Node/internal/fakebot"
"github.com/OpenVibers/OpenVibe.Node/internal/fakebot"
"github.com/OpenVibers/OpenVibe.Node/internal/protocol"
"github.com/OpenVibers/OpenVibe.Node/internal/protocol"
"github.com/OpenVibers/OpenVibe.Node/internal/safety"
"github.com/OpenVibers/OpenVibe.Node/internal/safety"
+ "github.com/OpenVibers/OpenVibe.Node/internal/service"
"github.com/OpenVibers/OpenVibe.Node/internal/video"
"github.com/OpenVibers/OpenVibe.Node/internal/video"
)
)
@@ -302,3 +303,28 @@ func TestRunDryRun(t *testing.T) {
@@ -302,3 +303,28 @@ func TestRunDryRun(t *testing.T) {
t.Fatalf("no shutdown stop in logs:\n%s", logs.String())
t.Fatalf("no shutdown stop in logs:\n%s", logs.String())
}
}
}
}
+
+// A pairing in its own --home never restarts the machine's node service: the service has one name whatever home it runs,
+// so it may be another node's (11-02: a test node paired in its own folder beside a working robot's service).
+func TestPairInAnotherHomeLeavesTheServiceAlone(t *testing.T) {
+ srv := fakebot.New()
+ defer srv.Close()
+ srv.AddCode("WXYZ2345")
+ home := shortHome(t)
+ t.Setenv("PYTHONPATH", pythonPath(t))
+ var controls []string
+ oldStatus, oldControl := serviceStatus, serviceControl
+ serviceStatus = func(service.Options) string { return "running" }
+ serviceControl = func(_ service.Options, action string) error { controls = append(controls, action); return nil }
+ defer func() { serviceStatus, serviceControl = oldStatus, oldControl }()
+ out, errOut, code := cli(t, "pair", "--code", "wxyz-2345", "--robot", fakebot.RobotID, "--home", home, "--server", srv.URL())
+ if code != 0 {
+ t.Fatalf("pair failed: %s", errOut)
+ }
+ if len(controls) != 0 {
+ t.Fatalf("the running service was touched: %v", controls)
+ }
+ if !strings.Contains(out, "left alone") || strings.Contains(out, "has been restarted") {
+ t.Fatalf("the output does not say the service was left alone: %s", out)
+ }
+}