openvibers/openvibe.link Commits

Link: Desktop and Terminal turn on again, after a plain-words yes; Connect defaults to openvibe.link (v0.48.0) (#139)

OP OpenVibers committed 5 days ago · fec2fa6dc6 · 12 files, +53 −36

The 11-05 review's ten findings are fixed (Node #102-#104, 11-06), so the gate that let the window turn Desktop and
Terminal off but never on is gone. Turning either on now shows what it means (the capability's confirm text) and
waits for a yes; the first-run answer still never turns them on. openvibe-node share's default tunnel, node API
base and messages point at connect.openvibe.link (the Website names keep working). v0.48.0.

Changes

ddiff --git a/STATUS.json b/STATUS.json
iindex 9865d51..e59c8b2 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -5,8 +5,8 @@
"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.47.0",
-- "release": "v0.47.0: `openvibe-node share` asks for the openvibe.link audience and prints share addresses on openvibe.link (`<label>--<user>.openvibe.link`; the openvibe.website address keeps working, #137); Link answers HO12 camera and location requests on the phone (#138).",
++ "version": "0.48.0",
++ "release": "v0.48.0: Desktop and Terminal can be turned on again from the Link window, after a plain-words confirmation, now that the 11-05 review fixes are in (#102-#104); the first-run answer never turns them on. `openvibe-node share` defaults to connect.openvibe.link.",
"runtime": "go 1.24 (one static binary per platform); plugins in Python 3; an ESP32 Arduino library",
"database": "none",
"deployed": false,
ddiff --git a/cmd/openvibe-node/main.go b/cmd/openvibe-node/main.go
iindex 6332414..b01c4ef 100644
---- a/cmd/openvibe-node/main.go
++++ b/cmd/openvibe-node/main.go
@@@ -194,9 +194,9 @@ func runWithStdin(args []string, stdin io.Reader, stdout, stderr io.Writer) int
session = fs.Bool("user-session", false, "Windows: start at sign-in in your own session instead of as a service (install, uninstall)")
keyID = fs.String("key-id", "", "accept only this Bot signing key id (profile verify)")
autoArg = fs.String("autostart", "", "on or off: start the tray when you sign in (tray)")
-- shKey = fs.String("key", "", "the share's ovc_… connect key from connect.openvibe.website (share)")
++ shKey = fs.String("key", "", "the share's ovc_… connect key from connect.openvibe.link (share)")
shPort = fs.Int("port", 0, "the local port to share (share)")
-- shSrv = fs.String("connect-server", "", "OpenVibe Connect's tunnel URL; default wss://connect.openvibe.website/tunnel (share)")
++ shSrv = fs.String("connect-server", "", "OpenVibe Connect's tunnel URL; default wss://connect.openvibe.link/tunnel (share)")
shID = fs.String("share", "", "a share this computer serves itself, as OpenVibe Link: its shr_… id, no key (share)")
)
// Accept flags anywhere: split positionals out.
ddiff --git a/cmd/openvibe-node/share.go b/cmd/openvibe-node/share.go
iindex 08c2994..7422571 100644
---- a/cmd/openvibe-node/share.go
++++ b/cmd/openvibe-node/share.go
@@@ -36,7 +36,7 @@ const shareUsage = `usage:
openvibe-link share list the shares the service keeps up
openvibe-link share remove <PORT|KEY|ID|ADDRESS> stop keeping one up
--Make a share at https://connect.openvibe.website; its address is https://<label>--<you>.openvibe.link. On its page, pick this computer under "Or serve it from one of your
++Make a share at https://connect.openvibe.link; its address is https://<label>--<you>.openvibe.link. On its page, pick this computer under "Or serve it from one of your
computers" to serve it with --share and no key (the computer must be added on openvibe.bot/machines). Without --port,
the Node serves the port the share's page names now, and keeps that one. The Node only ever connects to localhost on
the share's port; every visitor signs in with OpenVibe first.`
@@@ -61,7 +61,7 @@ func connectNodeToken(creds *credentials.Credentials) func(context.Context) (str
// share's page's now).
func nodeShare(ctx context.Context, g *globals, a shareArgs) (connect.Share, func(context.Context) (string, error), error) {
if !connect.ShareIDRe.MatchString(a.share) {
-- return connect.Share{}, nil, errors.New("--share is the share's shr_… id, from its page on connect.openvibe.website")
++ return connect.Share{}, nil, errors.New("--share is the share's shr_… id, from its page on connect.openvibe.link")
}
creds, err := credentials.Load(g.paths.CredentialFile(), nil)
if err != nil || !creds.NetworkPaired() {
@@@ -84,7 +84,7 @@ func nodeShare(ctx context.Context, g *globals, a shareArgs) (connect.Share, fun
}
}
if s.Port == 0 {
-- return connect.Share{}, nil, fmt.Errorf("this computer does not serve %s: pick it on the share's page on connect.openvibe.website", a.share)
++ return connect.Share{}, nil, fmt.Errorf("this computer does not serve %s: pick it on the share's page on connect.openvibe.link", a.share)
}
}
return s, tokens, nil
@@@ -117,7 +117,7 @@ func shareByPort(ctx context.Context, g *globals, a shareArgs) (connect.Share, f
}
switch len(found) {
case 0:
-- return connect.Share{}, nil, fmt.Errorf("this computer does not serve localhost:%d: pick it on the share's page on connect.openvibe.website", a.port)
++ return connect.Share{}, nil, fmt.Errorf("this computer does not serve localhost:%d: pick it on the share's page on connect.openvibe.link", a.port)
case 1:
return connect.Share{ID: found[0].ID, Port: found[0].LocalPort, Server: a.server, Name: found[0].Host}, tokens, nil
default:
@@@ -173,7 +173,7 @@ func cmdShare(g *globals, positional []string, a shareArgs, stdout, stderr io.Wr
return err
}
if len(mine) == 0 {
-- fmt.Fprintln(stdout, "No shares picked this computer yet. On a share's page on connect.openvibe.website, pick it under \"Or serve it from one of your computers\".")
++ fmt.Fprintln(stdout, "No shares picked this computer yet. On a share's page on connect.openvibe.link, pick it under \"Or serve it from one of your computers\".")
return nil
}
for _, m := range mine {
@@@ -213,7 +213,7 @@ func cmdShare(g *globals, positional []string, a shareArgs, stdout, stderr io.Wr
return errUsage
}
if !strings.HasPrefix(a.key, "ovc_") {
-- return errors.New("the key is the share's ovc_… connect key, from its page on connect.openvibe.website")
++ return errors.New("the key is the share's ovc_… connect key, from its page on connect.openvibe.link")
}
if err := g.paths.Ensure(); err != nil {
return err
ddiff --git a/cmd/openvibe-node/share_test.go b/cmd/openvibe-node/share_test.go
iindex 72352e0..6ab2f2d 100644
---- a/cmd/openvibe-node/share_test.go
++++ b/cmd/openvibe-node/share_test.go
@@@ -17,7 +17,7 @@ func TestShareCommands(t *testing.T) {
key := "ovc_" + strings.Repeat("K", 43)
var out, errb bytes.Buffer
-- if code := run([]string{"share", "--home", home}, &out, &errb); code != 2 || !strings.Contains(errb.String(), "connect.openvibe.website") {
++ if code := run([]string{"share", "--home", home}, &out, &errb); code != 2 || !strings.Contains(errb.String(), "connect.openvibe.link") {
t.Fatalf("usage: %d %q", code, errb.String())
}
out.Reset()
ddiff --git a/docs/panel.md b/docs/panel.md
iindex d0c2267..cac87b5 100644
---- a/docs/panel.md
++++ b/docs/panel.md
@@@ -61,6 +61,7 @@ Turning one **off** adds a local restriction the Node enforces itself: a command
`not_allowed` before it reaches the plugin. Turning it **on** removes only that restriction; it never grants more than
the plugin already offers, and it cannot raise the computer's level.
--**Desktop (`desk`) and Terminal (`terminal`) are gated** on the 11-05 review fixes: they may be turned off here but
--never on (`409` with the reason). At level `observe` nothing can be turned on from the panel: the level is set on the
--machine.
++**Desktop (`desk`) and Terminal (`terminal`)** turn on only by their own switch, after the window says in plain words
++what that means (the capability's `confirm` text); the first-run answer never turns them on. They were gated until
++the 11-05 review's fixes landed (Node #102-#104, 2026-11-06); [computer.md](computer.md) says what a program run at
++`trusted` can reach. At level `observe` nothing can be turned on from the panel: the level is set on the machine.
ddiff --git a/internal/connect/connect.go b/internal/connect/connect.go
iindex 163b2db..2e6326b 100644
---- a/internal/connect/connect.go
++++ b/internal/connect/connect.go
@@@ -1,7 +1,7 @@
// Package connect keeps OpenVibe Connect shares up from the Node: each share is one port on this machine served at
// https://<label>--<user>.openvibe.link behind OpenVibe sign-in (OpenVibe.Connect, ADR-057, Website Connect v0).
//
--// The Node opens one outgoing WebSocket per share to wss://connect.openvibe.website/tunnel and speaks ovconnect-ws/1,
++// The Node opens one outgoing WebSocket per share to wss://connect.openvibe.link/tunnel and speaks ovconnect-ws/1,
// the same protocol as the one-file client (OpenVibe.Connect public/ov-connect.js):
//
// → {"t":"hello","v":1,"key":"ovc_…","client":"openvibe-node/…","port":8790}
@@@ -39,7 +39,7 @@ import (
)
// DefaultServer is OpenVibe Connect's tunnel endpoint.
--const DefaultServer = "wss://connect.openvibe.website/tunnel"
++const DefaultServer = "wss://connect.openvibe.link/tunnel"
const (
protocolVersion = 1
@@@ -103,7 +103,7 @@ func Run(ctx context.Context, s Share, o Options) error {
switch {
case s.ID != "":
if !ShareIDRe.MatchString(s.ID) {
-- return errors.New("connect: the share is its shr_… id from connect.openvibe.website")
++ return errors.New("connect: the share is its shr_… id from connect.openvibe.link")
}
if o.NodeToken == nil {
return errors.New("connect: this share is served by the computer itself; pair the Node with OpenVibe first (openvibe.bot/machines)")
ddiff --git a/internal/connect/node.go b/internal/connect/node.go
iindex 19cf6fe..a9938f5 100644
---- a/internal/connect/node.go
++++ b/internal/connect/node.go
@@@ -4,7 +4,7 @@ package connect
// computer on the share's page; there is no connect key. Before every connection the Node buys its node token (subject
// node:nod_…, audience openvibe.link) and asks Connect for a tunnel token for that one share:
//
--// POST https://connect.openvibe.website/api/v1/node/shares/<shr_…>/tunnel-token Authorization: Bearer <node token>
++// POST https://connect.openvibe.link/api/v1/node/shares/<shr_…>/tunnel-token Authorization: Bearer <node token>
// → {"tunnel_token":"ovt_…","expires_in":60,"share":{"id","host","url","local_port","mode"}}
//
// then says hello with both, {"t":"hello","v":1,"share":"shr_…","node_token":…,"tunnel":"ovt_…",…}, and sends
ddiff --git a/internal/connect/node_test.go b/internal/connect/node_test.go
iindex 3c5c2bc..f0d379f 100644
---- a/internal/connect/node_test.go
++++ b/internal/connect/node_test.go
@@@ -178,7 +178,7 @@ func TestNodeSharesAndTheAPIURL(t *testing.T) {
}
for in, want := range map[string]string{
"wss://connect.openvibe.website/tunnel": "https://connect.openvibe.website/x",
-- "": "https://connect.openvibe.website/x",
++ "": "https://connect.openvibe.link/x",
"ws://127.0.0.1:5240/tunnel": "http://127.0.0.1:5240/x",
} {
if u, err := apiURL(in, "/x"); err != nil || u != want {
ddiff --git a/internal/node/panel.go b/internal/node/panel.go
iindex 9e62494..a6931f7 100644
---- a/internal/node/panel.go
++++ b/internal/node/panel.go
@@@ -38,8 +38,13 @@ var panelCapabilityList = []struct {
{"ai", "Run AI models here", "Only you, from your other devices", false},
}
--// panelGated are the switches Link may turn off but never on: Desktop and Terminal wait for the 11-05 review fixes.
--var panelGated = map[string]bool{"desk": true, "terminal": true}
++// panelAskFirst are the switches the window confirms in plain words before turning on. They were gated until the
++// 11-05 review's fixes (Node #102-#104, merged 11-06); they are never turned on by the first-run answer, only by their
++// own switch.
++var panelAskFirst = map[string]string{
++ "terminal": "Turn on the terminal? Your agents can ask to run commands on this computer. At level ask you approve each one; at trusted they run with your account's full reach.",
++ "desk": "Turn on the desk? Your agents get their own desktop off your screen, with its own browser, and work there at the level you set.",
++}
// capabilityForKind maps a command kind to the switch that can refuse it.
var capabilityForKind = map[string]string{
@@@ -260,7 +265,7 @@ func (n *Node) panelCapabilities(st Status) []panel.Capability {
if c.computer && (level == "" || level == "observe") {
on = false
}
-- out = append(out, panel.Capability{ID: c.ID, Name: c.Name, Who: c.Who, On: on})
++ out = append(out, panel.Capability{ID: c.ID, Name: c.Name, Who: c.Who, On: on, Confirm: panelAskFirst[c.ID]})
}
return out
}
@@@ -276,9 +281,6 @@ func (n *Node) setCapability(id string, on bool) error {
if !known {
return fmt.Errorf("no capability %q on this computer", id)
}
-- if on && panelGated[id] {
-- return fmt.Errorf("%s is gated until the 11-05 review fixes are in: it cannot be turned on from Link", id)
-- }
if on && computer && n.computerLevel() == "observe" {
return errors.New("this computer is at level observe: set its level on the machine first")
}
@@@ -310,7 +312,7 @@ func (n *Node) setCapability(id string, on bool) error {
// applyFirstRun records the answer to "What's this computer for?" and turns on what each use says: a browser for the
// agents, or the AI plugin. Codes has no module switch on the Node yet and earn only records the wish (Exchange
--// confirms it); Terminal and Desktop are never turned on here, as they stay gated on the 11-05 review. A skip only
++// confirms it); Terminal and Desktop are never turned on here, only by their own switch in the window. A skip only
// stops the question. One node.first_run event carries the answer.
func (n *Node) applyFirstRun(uses []string, skip bool) error {
if skip {
@@@ -329,7 +331,7 @@ func (n *Node) applyFirstRun(uses []string, skip bool) error {
n.log.Warn("first run: models stay off", "err", err)
}
case "codes":
-- // Recorded only: the Node has no Codes module to switch on, and Terminal stays as the gate allows.
++ // Recorded only: the Node has no Codes module to switch on, and Terminal stays as it is (its own switch).
case "earn":
// Recorded only: Exchange confirms it, and nothing on this machine turns on here.
}
ddiff --git a/internal/node/panel_test.go b/internal/node/panel_test.go
iindex c156ab8..0537348 100644
---- a/internal/node/panel_test.go
++++ b/internal/node/panel_test.go
@@@ -94,15 +94,23 @@ func TestCapabilitySwitchIsARealRestriction(t *testing.T) {
if n2.capabilityOff(protocol.KindFS) {
t.Fatal("files on still refuses fs commands")
}
-- // Desktop and Terminal may be turned off but never on from Link.
-- if err := n2.setCapability("terminal", false); err != nil {
-- t.Fatal(err)
++ // Desktop and Terminal turn off and back on from Link (the 11-05 fixes are in); the window asks first.
++ for _, id := range []string{"terminal", "desk"} {
++ if err := n2.setCapability(id, false); err != nil {
++ t.Fatal(err)
++ }
++ if err := n2.setCapability(id, true); err != nil {
++ t.Fatalf("%s on: %v", id, err)
++ }
}
-- if err := n2.setCapability("terminal", true); err == nil || !strings.Contains(err.Error(), "gated") {
-- t.Fatalf("terminal on: %v", err)
++ if n2.capabilityOff(protocol.KindExec) || n2.capabilityOff(protocol.KindDesk) {
++ t.Fatal("terminal or desk still refused after turning them on")
}
-- if err := n2.setCapability("desk", true); err == nil || !strings.Contains(err.Error(), "gated") {
-- t.Fatalf("desk on: %v", err)
++ offer := Status{Plugins: []PluginStatus{{Capabilities: map[string]any{"terminal": true, "desk": true, "files": true}}}}
++ for _, c := range n2.panelCapabilities(offer) {
++ if (c.ID == "terminal" || c.ID == "desk") != (c.Confirm != "") {
++ t.Fatalf("%s: confirm %q", c.ID, c.Confirm)
++ }
}
if err := n2.setCapability("nope", true); err == nil {
t.Fatal("an unknown capability was accepted")
@@@ -199,7 +207,7 @@ func TestFirstRunTurnsOnOnlyWhatEachUseSays(t *testing.T) {
if n.capabilityOff(protocol.KindAI) {
t.Fatal("models were not turned on")
}
-- // Codes and earn record the wish only; Terminal and Desktop stay exactly as the gate allows (off).
++ // Codes and earn record the wish only; an answer never turns Terminal or Desktop on (only their own switch).
if !n.capabilityOff(protocol.KindExec) {
t.Fatal("the answer turned Terminal on")
}
ddiff --git a/internal/panel/panel.go b/internal/panel/panel.go
iindex 0f050ce..77223fd 100644
---- a/internal/panel/panel.go
++++ b/internal/panel/panel.go
@@@ -61,6 +61,8 @@ type Capability struct {
Name string `json:"name"`
Who string `json:"who,omitempty"`
On bool `json:"on"`
++ // Confirm is what the window asks, in plain words, before turning this switch on (Terminal and Desktop).
++ Confirm string `json:"confirm,omitempty"`
}
// Share is one port put on the web through OpenVibe Connect.
ddiff --git a/internal/panel/ui/panel.js b/internal/panel/ui/panel.js
iindex 9057531..c882afa 100644
---- a/internal/panel/ui/panel.js
++++ b/internal/panel/ui/panel.js
@@@ -102,7 +102,11 @@
$('[data-lk-caps]').replaceChildren.apply($('[data-lk-caps]'), caps.map(function (c) {
var li = el('li', 'lk-cap'); var t = el('span'); t.append(el('b', '', c.name || c.id), el('small', '', c.who || ''));
var sw = el('label', 'lk-switch'); var box = el('input'); box.type = 'checkbox'; box.checked = !!c.on; box.setAttribute('aria-label', c.name || c.id);
-- box.addEventListener('change', function () { send('capabilities/' + encodeURIComponent(c.id), 'POST', { on: box.checked }).catch(function () { box.checked = !box.checked; }); });
++ box.addEventListener('change', function () {
++ // Terminal and Desktop say what turning them on means, and wait for a yes.
++ if (box.checked && c.confirm && !window.confirm(c.confirm)) { box.checked = false; return; }
++ send('capabilities/' + encodeURIComponent(c.id), 'POST', { on: box.checked }).catch(function () { box.checked = !box.checked; });
++ });
sw.append(box, el('i')); li.append(t, sw); return li;
}));
root.querySelector('[data-lk-sec="caps"]').hidden = caps.length === 0;