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)
Changes
@@@ -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,@@@ -194,9 +194,9 @@ func runWithStdin(args []string, stdin io.Reader, stdout, stderr io.Writer) intsession = 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.@@@ -36,7 +36,7 @@ const shareUsage = `usage:openvibe-link share list the shares the service keeps upopenvibe-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 yourcomputers" 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 onthe 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, nildefault:@@@ -173,7 +173,7 @@ func cmdShare(g *globals, positional []string, a shareArgs, stdout, stderr io.Wrreturn 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.Wrreturn 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@@@ -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()@@@ -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 thanthe 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.@@@ -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)")@@@ -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@@@ -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 {@@@ -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.}@@@ -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")}@@@ -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.@@@ -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;
@@ -5,8 +5,8 @@
@@ -5,8 +5,8 @@
"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.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",
"runtime": "go 1.24 (one static binary per platform); plugins in Python 3; an ESP32 Arduino library",
"database": "none",
"database": "none",
"deployed": false,
"deployed": false,
@@ -194,9 +194,9 @@ func runWithStdin(args []string, stdin io.Reader, stdout, stderr io.Writer) int
@@ -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)")
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)")
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)")
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)")
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)")
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.
// Accept flags anywhere: split positionals out.
@@ -36,7 +36,7 @@ const shareUsage = `usage:
@@ -36,7 +36,7 @@ const shareUsage = `usage:
openvibe-link share list the shares the service keeps up
openvibe-link share list the shares the service keeps up
openvibe-link share remove <PORT|KEY|ID|ADDRESS> stop keeping one 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,
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 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.`
the share's port; every visitor signs in with OpenVibe first.`
@@ -61,7 +61,7 @@ func connectNodeToken(creds *credentials.Credentials) func(context.Context) (str
@@ -61,7 +61,7 @@ func connectNodeToken(creds *credentials.Credentials) func(context.Context) (str
// share's page's now).
// share's page's now).
func nodeShare(ctx context.Context, g *globals, a shareArgs) (connect.Share, func(context.Context) (string, error), error) {
func nodeShare(ctx context.Context, g *globals, a shareArgs) (connect.Share, func(context.Context) (string, error), error) {
if !connect.ShareIDRe.MatchString(a.share) {
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)
creds, err := credentials.Load(g.paths.CredentialFile(), nil)
if err != nil || !creds.NetworkPaired() {
if err != nil || !creds.NetworkPaired() {
@@ -84,7 +84,7 @@ func nodeShare(ctx context.Context, g *globals, a shareArgs) (connect.Share, fun
@@ -84,7 +84,7 @@ func nodeShare(ctx context.Context, g *globals, a shareArgs) (connect.Share, fun
}
}
}
}
if s.Port == 0 {
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
return s, tokens, nil
@@ -117,7 +117,7 @@ func shareByPort(ctx context.Context, g *globals, a shareArgs) (connect.Share, f
@@ -117,7 +117,7 @@ func shareByPort(ctx context.Context, g *globals, a shareArgs) (connect.Share, f
}
}
switch len(found) {
switch len(found) {
case 0:
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:
case 1:
return connect.Share{ID: found[0].ID, Port: found[0].LocalPort, Server: a.server, Name: found[0].Host}, tokens, nil
return connect.Share{ID: found[0].ID, Port: found[0].LocalPort, Server: a.server, Name: found[0].Host}, tokens, nil
default:
default:
@@ -173,7 +173,7 @@ func cmdShare(g *globals, positional []string, a shareArgs, stdout, stderr io.Wr
@@ -173,7 +173,7 @@ func cmdShare(g *globals, positional []string, a shareArgs, stdout, stderr io.Wr
return err
return err
}
}
if len(mine) == 0 {
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
return nil
}
}
for _, m := range mine {
for _, m := range mine {
@@ -213,7 +213,7 @@ func cmdShare(g *globals, positional []string, a shareArgs, stdout, stderr io.Wr
@@ -213,7 +213,7 @@ func cmdShare(g *globals, positional []string, a shareArgs, stdout, stderr io.Wr
return errUsage
return errUsage
}
}
if !strings.HasPrefix(a.key, "ovc_") {
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 {
if err := g.paths.Ensure(); err != nil {
return err
return err
@@ -17,7 +17,7 @@ func TestShareCommands(t *testing.T) {
@@ -17,7 +17,7 @@ func TestShareCommands(t *testing.T) {
key := "ovc_" + strings.Repeat("K", 43)
key := "ovc_" + strings.Repeat("K", 43)
var out, errb bytes.Buffer
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())
t.Fatalf("usage: %d %q", code, errb.String())
}
}
out.Reset()
out.Reset()
@@ -61,6 +61,7 @@ Turning one **off** adds a local restriction the Node enforces itself: a command
@@ -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
`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.
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.
@@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
// Package connect keeps OpenVibe Connect shares up from the Node: each share is one port on this machine served at
// 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).
// 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):
// 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}
// → {"t":"hello","v":1,"key":"ovc_…","client":"openvibe-node/…","port":8790}
@@ -39,7 +39,7 @@ import (
@@ -39,7 +39,7 @@ import (
)
)
// DefaultServer is OpenVibe Connect's tunnel endpoint.
// DefaultServer is OpenVibe Connect's tunnel endpoint.
-const DefaultServer = "wss://connect.openvibe.website/tunnel"
+const DefaultServer = "wss://connect.openvibe.link/tunnel"
const (
const (
protocolVersion = 1
protocolVersion = 1
@@ -103,7 +103,7 @@ func Run(ctx context.Context, s Share, o Options) error {
@@ -103,7 +103,7 @@ func Run(ctx context.Context, s Share, o Options) error {
switch {
switch {
case s.ID != "":
case s.ID != "":
if !ShareIDRe.MatchString(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 {
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)")
return errors.New("connect: this share is served by the computer itself; pair the Node with OpenVibe first (openvibe.bot/machines)")
@@ -4,7 +4,7 @@ package connect
@@ -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
// 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:
// 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"}}
// → {"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
// then says hello with both, {"t":"hello","v":1,"share":"shr_…","node_token":…,"tunnel":"ovt_…",…}, and sends
@@ -178,7 +178,7 @@ func TestNodeSharesAndTheAPIURL(t *testing.T) {
@@ -178,7 +178,7 @@ func TestNodeSharesAndTheAPIURL(t *testing.T) {
}
}
for in, want := range map[string]string{
for in, want := range map[string]string{
"wss://connect.openvibe.website/tunnel": "https://connect.openvibe.website/x",
"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",
"ws://127.0.0.1:5240/tunnel": "http://127.0.0.1:5240/x",
} {
} {
if u, err := apiURL(in, "/x"); err != nil || u != want {
if u, err := apiURL(in, "/x"); err != nil || u != want {
@@ -38,8 +38,13 @@ var panelCapabilityList = []struct {
@@ -38,8 +38,13 @@ var panelCapabilityList = []struct {
{"ai", "Run AI models here", "Only you, from your other devices", false},
{"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.
// capabilityForKind maps a command kind to the switch that can refuse it.
var capabilityForKind = map[string]string{
var capabilityForKind = map[string]string{
@@ -260,7 +265,7 @@ func (n *Node) panelCapabilities(st Status) []panel.Capability {
@@ -260,7 +265,7 @@ func (n *Node) panelCapabilities(st Status) []panel.Capability {
if c.computer && (level == "" || level == "observe") {
if c.computer && (level == "" || level == "observe") {
on = false
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
return out
}
}
@@ -276,9 +281,6 @@ func (n *Node) setCapability(id string, on bool) error {
@@ -276,9 +281,6 @@ func (n *Node) setCapability(id string, on bool) error {
if !known {
if !known {
return fmt.Errorf("no capability %q on this computer", id)
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" {
if on && computer && n.computerLevel() == "observe" {
return errors.New("this computer is at level observe: set its level on the machine first")
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 {
@@ -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
// 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
// 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.
// stops the question. One node.first_run event carries the answer.
func (n *Node) applyFirstRun(uses []string, skip bool) error {
func (n *Node) applyFirstRun(uses []string, skip bool) error {
if skip {
if skip {
@@ -329,7 +331,7 @@ func (n *Node) applyFirstRun(uses []string, skip bool) error {
@@ -329,7 +331,7 @@ func (n *Node) applyFirstRun(uses []string, skip bool) error {
n.log.Warn("first run: models stay off", "err", err)
n.log.Warn("first run: models stay off", "err", err)
}
}
case "codes":
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":
case "earn":
// Recorded only: Exchange confirms it, and nothing on this machine turns on here.
// Recorded only: Exchange confirms it, and nothing on this machine turns on here.
}
}
@@ -94,15 +94,23 @@ func TestCapabilitySwitchIsARealRestriction(t *testing.T) {
@@ -94,15 +94,23 @@ func TestCapabilitySwitchIsARealRestriction(t *testing.T) {
if n2.capabilityOff(protocol.KindFS) {
if n2.capabilityOff(protocol.KindFS) {
t.Fatal("files on still refuses fs commands")
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 {
if err := n2.setCapability("nope", true); err == nil {
t.Fatal("an unknown capability was accepted")
t.Fatal("an unknown capability was accepted")
@@ -199,7 +207,7 @@ func TestFirstRunTurnsOnOnlyWhatEachUseSays(t *testing.T) {
@@ -199,7 +207,7 @@ func TestFirstRunTurnsOnOnlyWhatEachUseSays(t *testing.T) {
if n.capabilityOff(protocol.KindAI) {
if n.capabilityOff(protocol.KindAI) {
t.Fatal("models were not turned on")
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) {
if !n.capabilityOff(protocol.KindExec) {
t.Fatal("the answer turned Terminal on")
t.Fatal("the answer turned Terminal on")
}
}
@@ -61,6 +61,8 @@ type Capability struct {
@@ -61,6 +61,8 @@ type Capability struct {
Name string `json:"name"`
Name string `json:"name"`
Who string `json:"who,omitempty"`
Who string `json:"who,omitempty"`
On bool `json:"on"`
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.
// Share is one port put on the web through OpenVibe Connect.
@@ -102,7 +102,11 @@
@@ -102,7 +102,11 @@
$('[data-lk-caps]').replaceChildren.apply($('[data-lk-caps]'), caps.map(function (c) {
$('[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 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);
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;
sw.append(box, el('i')); li.append(t, sw); return li;
}));
}));
root.querySelector('[data-lk-sec="caps"]').hidden = caps.length === 0;
root.querySelector('[data-lk-sec="caps"]').hidden = caps.length === 0;