openvibers/openvibe.link Commits
Keep plan logins out of skill runner jobs (#154)
Changes
@@@ -95,7 +95,8 @@ write any file (in the job directory included), read anything outside the job diThe runner adds no network of its own: the tool has the job sandbox's network namespace, so under `net: deny` (thedefault) there is no network at all, and under `public` or `openvibe-only` only what the job's `net` and the owner'segress allow. The tool also has the job's uid/gid, private root, seccomp filter, cgroups and rlimits; it sees no Link--credential, no owner home and no state.++credential, no owner home and no state. The worker does not stage `worker.plan_logins` for skill runner jobs, even when++the owner enabled plan logins for other functions.The tool writes **one JSON value to stdout, at most 64 KiB**. More is stopped and the result says `limit`; stderr iskept for the person, cut at 16 KiB. The runner writes the job's result to fd 3:@@@ -6,8 +6,18 @@ import ("os""path/filepath""strings"++++ "github.com/OpenVibers/OpenVibe.Link/internal/config")++// Skill code can read its entire job directory. It must never receive an owner's plan-login files.++func stageJobPlanLogins(fn config.FunctionConfig, logins []string, root string) ([]string, error) {++ if fn.Name == "openvibe-skill-runner" {++ return nil, nil++ }++ return stagePlanLogins(logins, root)++}++// The plan logins: the owner's own coding-CLI sign-in files, copied read-only into a job's private home.//// A CLI signs in with a file in the owner's home (~/.claude/.credentials.json, ~/.codex/auth.json,@@@ -4,8 +4,34 @@ import ("os""path/filepath""testing"++++ "github.com/OpenVibers/OpenVibe.Link/internal/config")++func TestSkillRunnerNeverStagesPlanLogins(t *testing.T) {++ home := t.TempDir()++ setHome(t, home)++ login := filepath.Join(home, ".codex", "auth.json")++ if err := os.MkdirAll(filepath.Dir(login), 0o700); err != nil {++ t.Fatal(err)++ }++ if err := os.WriteFile(login, []byte(`{"test":"login"}`), 0o600); err != nil {++ t.Fatal(err)++ }++ root := t.TempDir()++ names, err := stageJobPlanLogins(config.FunctionConfig{Name: "openvibe-skill-runner", Version: "1.0.0"}, []string{login}, root)++ if err != nil || len(names) != 0 {++ t.Fatalf("skill runner staged a login: names=%v err=%v", names, err)++ }++ if _, err := os.Stat(filepath.Join(root, ".codex", "auth.json")); !os.IsNotExist(err) {++ t.Fatalf("skill runner left a login in its job directory: %v", err)++ }++ names, err = stageJobPlanLogins(config.FunctionConfig{Name: "openvibe-codes"}, []string{login}, root)++ if err != nil || len(names) != 1 {++ t.Fatalf("ordinary function lost the owner's opt-in: names=%v err=%v", names, err)++ }++}++// The plan logins: the copy, the read-only mode, the cleanup and the opt-in.// setHome points the Link user's home at dir: os.UserHomeDir reads HOME on Linux and macOS, USERPROFILE on Windows.@@@ -5,6 +5,7 @@ package workerimport ("encoding/json""os"++ "path/filepath""strings""testing"@@@ -53,6 +54,37 @@ func skillRunnerWorker(t *testing.T, m *mediaFake) (*Worker, *sink) {return w, s}++// The owner may share coding CLI logins with other functions. Skill code must never receive those files.++func TestSkillRunnerJobHasNoPlanLogins(t *testing.T) {++ home := t.TempDir()++ setHome(t, home)++ login := filepath.Join(home, ".codex", "auth.json")++ if err := os.MkdirAll(filepath.Dir(login), 0o700); err != nil {++ t.Fatal(err)++ }++ if err := os.WriteFile(login, []byte(`{"test":"login"}`), 0o600); err != nil {++ t.Fatal(err)++ }++ tool := []byte(`const fs = require('fs'); process.stdout.write(JSON.stringify({present: fs.existsSync('.codex/auth.json')}));`)++ m := newMediaFake(t)++ m.objects[inputID] = tool++ t.Setenv(testMediaEnv, testMediaToken)++ w, s := skillRunnerWorker(t, m)++ w.cfg.PlanLogins = []string{login}++ j := skillJob(4, tool, digestOf(tool), `{"tool":"check","entry":"code.js","input":{}}`)++ run(t, w, j)++ ex := s.exit(t, j.ID)++ var got struct {++ OK bool `json:"ok"`++ Output struct {++ Present bool `json:"present"`++ } `json:"output"`++ }++ if err := json.Unmarshal(ex.Result, &got); err != nil || !got.OK || got.Output.Present {++ t.Fatalf("the skill job received a plan login: %+v result %s: %v", ex, ex.Result, err)++ }++}++// skillJob is one job of the declared runner: code.js is the tool's code, pinned by digest, and args names it.func skillJob(n int, tool []byte, digest string, args string) protocol.Job {j := testJob(n, skills.Name)@@@ -429,9 +429,7 @@ func (w *Worker) execute(jb *job) protocol.JobExit {}return never(protocol.ExitFailed, err)}-- // The owner's plan logins, staged for this job only: the copy goes with the job's directory and its content-- // never leaves the machine.-- planLogins, err := stagePlanLogins(w.cfg.PlanLogins, dir)++ planLogins, err := stageJobPlanLogins(jb.fn, w.cfg.PlanLogins, dir)if err != nil {return never(protocol.ExitFailed, err)}
@@ -95,7 +95,8 @@ write any file (in the job directory included), read anything outside the job di
@@ -95,7 +95,8 @@ write any file (in the job directory included), read anything outside the job di
The runner adds no network of its own: the tool has the job sandbox's network namespace, so under `net: deny` (the
The runner adds no network of its own: the tool has the job sandbox's network namespace, so under `net: deny` (the
default) there is no network at all, and under `public` or `openvibe-only` only what the job's `net` and the owner's
default) there is no network at all, and under `public` or `openvibe-only` only what the job's `net` and the owner's
egress allow. The tool also has the job's uid/gid, private root, seccomp filter, cgroups and rlimits; it sees no Link
egress allow. The tool also has the job's uid/gid, private root, seccomp filter, cgroups and rlimits; it sees no Link
-credential, no owner home and no state.
+credential, no owner home and no state. The worker does not stage `worker.plan_logins` for skill runner jobs, even when
+the owner enabled plan logins for other functions.
The tool writes **one JSON value to stdout, at most 64 KiB**. More is stopped and the result says `limit`; stderr is
The tool writes **one JSON value to stdout, at most 64 KiB**. More is stopped and the result says `limit`; stderr is
kept for the person, cut at 16 KiB. The runner writes the job's result to fd 3:
kept for the person, cut at 16 KiB. The runner writes the job's result to fd 3:
@@ -6,8 +6,18 @@ import (
@@ -6,8 +6,18 @@ import (
"os"
"os"
"path/filepath"
"path/filepath"
"strings"
"strings"
+
+ "github.com/OpenVibers/OpenVibe.Link/internal/config"
)
)
+// Skill code can read its entire job directory. It must never receive an owner's plan-login files.
+func stageJobPlanLogins(fn config.FunctionConfig, logins []string, root string) ([]string, error) {
+ if fn.Name == "openvibe-skill-runner" {
+ return nil, nil
+ }
+ return stagePlanLogins(logins, root)
+}
+
// The plan logins: the owner's own coding-CLI sign-in files, copied read-only into a job's private home.
// The plan logins: the owner's own coding-CLI sign-in files, copied read-only into a job's private home.
//
//
// A CLI signs in with a file in the owner's home (~/.claude/.credentials.json, ~/.codex/auth.json,
// A CLI signs in with a file in the owner's home (~/.claude/.credentials.json, ~/.codex/auth.json,
@@ -4,8 +4,34 @@ import (
@@ -4,8 +4,34 @@ import (
"os"
"os"
"path/filepath"
"path/filepath"
"testing"
"testing"
+
+ "github.com/OpenVibers/OpenVibe.Link/internal/config"
)
)
+func TestSkillRunnerNeverStagesPlanLogins(t *testing.T) {
+ home := t.TempDir()
+ setHome(t, home)
+ login := filepath.Join(home, ".codex", "auth.json")
+ if err := os.MkdirAll(filepath.Dir(login), 0o700); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(login, []byte(`{"test":"login"}`), 0o600); err != nil {
+ t.Fatal(err)
+ }
+ root := t.TempDir()
+ names, err := stageJobPlanLogins(config.FunctionConfig{Name: "openvibe-skill-runner", Version: "1.0.0"}, []string{login}, root)
+ if err != nil || len(names) != 0 {
+ t.Fatalf("skill runner staged a login: names=%v err=%v", names, err)
+ }
+ if _, err := os.Stat(filepath.Join(root, ".codex", "auth.json")); !os.IsNotExist(err) {
+ t.Fatalf("skill runner left a login in its job directory: %v", err)
+ }
+ names, err = stageJobPlanLogins(config.FunctionConfig{Name: "openvibe-codes"}, []string{login}, root)
+ if err != nil || len(names) != 1 {
+ t.Fatalf("ordinary function lost the owner's opt-in: names=%v err=%v", names, err)
+ }
+}
+
// The plan logins: the copy, the read-only mode, the cleanup and the opt-in.
// The plan logins: the copy, the read-only mode, the cleanup and the opt-in.
// setHome points the Link user's home at dir: os.UserHomeDir reads HOME on Linux and macOS, USERPROFILE on Windows.
// setHome points the Link user's home at dir: os.UserHomeDir reads HOME on Linux and macOS, USERPROFILE on Windows.
@@ -5,6 +5,7 @@ package worker
@@ -5,6 +5,7 @@ package worker
import (
import (
"encoding/json"
"encoding/json"
"os"
"os"
+ "path/filepath"
"strings"
"strings"
"testing"
"testing"
@@ -53,6 +54,37 @@ func skillRunnerWorker(t *testing.T, m *mediaFake) (*Worker, *sink) {
@@ -53,6 +54,37 @@ func skillRunnerWorker(t *testing.T, m *mediaFake) (*Worker, *sink) {
return w, s
return w, s
}
}
+// The owner may share coding CLI logins with other functions. Skill code must never receive those files.
+func TestSkillRunnerJobHasNoPlanLogins(t *testing.T) {
+ home := t.TempDir()
+ setHome(t, home)
+ login := filepath.Join(home, ".codex", "auth.json")
+ if err := os.MkdirAll(filepath.Dir(login), 0o700); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(login, []byte(`{"test":"login"}`), 0o600); err != nil {
+ t.Fatal(err)
+ }
+ tool := []byte(`const fs = require('fs'); process.stdout.write(JSON.stringify({present: fs.existsSync('.codex/auth.json')}));`)
+ m := newMediaFake(t)
+ m.objects[inputID] = tool
+ t.Setenv(testMediaEnv, testMediaToken)
+ w, s := skillRunnerWorker(t, m)
+ w.cfg.PlanLogins = []string{login}
+ j := skillJob(4, tool, digestOf(tool), `{"tool":"check","entry":"code.js","input":{}}`)
+ run(t, w, j)
+ ex := s.exit(t, j.ID)
+ var got struct {
+ OK bool `json:"ok"`
+ Output struct {
+ Present bool `json:"present"`
+ } `json:"output"`
+ }
+ if err := json.Unmarshal(ex.Result, &got); err != nil || !got.OK || got.Output.Present {
+ t.Fatalf("the skill job received a plan login: %+v result %s: %v", ex, ex.Result, err)
+ }
+}
+
// skillJob is one job of the declared runner: code.js is the tool's code, pinned by digest, and args names it.
// skillJob is one job of the declared runner: code.js is the tool's code, pinned by digest, and args names it.
func skillJob(n int, tool []byte, digest string, args string) protocol.Job {
func skillJob(n int, tool []byte, digest string, args string) protocol.Job {
j := testJob(n, skills.Name)
j := testJob(n, skills.Name)
@@ -429,9 +429,7 @@ func (w *Worker) execute(jb *job) protocol.JobExit {
@@ -429,9 +429,7 @@ func (w *Worker) execute(jb *job) protocol.JobExit {
}
}
return never(protocol.ExitFailed, err)
return never(protocol.ExitFailed, err)
}
}
- // The owner's plan logins, staged for this job only: the copy goes with the job's directory and its content
- // never leaves the machine.
- planLogins, err := stagePlanLogins(w.cfg.PlanLogins, dir)
+ planLogins, err := stageJobPlanLogins(jb.fn, w.cfg.PlanLogins, dir)
if err != nil {
if err != nil {
return never(protocol.ExitFailed, err)
return never(protocol.ExitFailed, err)
}
}