From 678b5c4aad2b6098bad4a9bdd2e56bc72bee7d53 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Fri, 9 Oct 2026 09:02:17 +0000 Subject: [PATCH] Provide local session system view --- apps/daemon/internal/agent/harness.go | 11 ++-- apps/daemon/internal/agent/view_test.go | 3 + apps/daemon/internal/agenthost/doc.go | 3 +- .../agenthost/environment_linux_test.go | 2 +- .../internal/sessionview/build_linux.go | 21 +++++++ apps/daemon/internal/sessionview/doc.go | 2 +- .../daemon/internal/sessionview/view_linux.go | 8 ++- .../internal/sessionview/view_linux_test.go | 63 ++++++++++++++++++- contracts/agents-api/harness-onboarding.md | 6 +- contracts/agents-api/zh/harness-onboarding.md | 8 ++- 10 files changed, 112 insertions(+), 15 deletions(-) diff --git a/apps/daemon/internal/agent/harness.go b/apps/daemon/internal/agent/harness.go index eebd4a70c..130d9d2f1 100644 --- a/apps/daemon/internal/agent/harness.go +++ b/apps/daemon/internal/agent/harness.go @@ -119,7 +119,7 @@ func (r *Registry) Register(declaration Declaration, runtime Runtime, environmen // Environment none. A request with DisableExecutionEnvironment runs in an // empty-root view: a read-only, noexec tmpfs root that holds only the // mountpoints for the closure, the home, the agent host's runtime files, -// ViewProcRoot, ViewDevRoot and the overlays. It has no world, no shims, no +// ViewProcRoot, ViewSysRoot, ViewDevRoot and the overlays. It has no world, no shims, no // Link attachment and no sandbox network, so the generic proxy refuses every // request; the cgroup, the isolation and the gateway stay. The Harness runs in // ViewPrivateRoot/ViewHomeName/ViewWorkName. A request with neither @@ -152,8 +152,11 @@ const ( // ViewWorkName is the working directory under the home in an empty-root // view. ViewWorkName = "work" - // ViewProcRoot and ViewDevRoot are the view's own /proc and minimal /dev. + // ViewProcRoot, ViewSysRoot and ViewDevRoot are the view's own kernel + // filesystems and minimal /dev. The read-only /sys/fs/cgroup exposes only + // the view's cgroup subtree, rooted in its cgroup namespace. ViewProcRoot = "/proc" + ViewSysRoot = "/sys" ViewDevRoot = "/dev" ) @@ -167,9 +170,9 @@ func ViewAlias(i int) string { } // ViewReserved reports whether the view path p is at or beneath a tree the -// view builds itself: ViewPrivateRoot, ViewProcRoot or ViewDevRoot. +// view builds itself: ViewPrivateRoot, ViewProcRoot, ViewSysRoot or ViewDevRoot. func ViewReserved(p string) bool { - for _, root := range [...]string{ViewPrivateRoot, ViewProcRoot, ViewDevRoot} { + for _, root := range [...]string{ViewPrivateRoot, ViewProcRoot, ViewSysRoot, ViewDevRoot} { if p == root || isWithin(p, root) { return true } diff --git a/apps/daemon/internal/agent/view_test.go b/apps/daemon/internal/agent/view_test.go index 629753f38..692328cae 100644 --- a/apps/daemon/internal/agent/view_test.go +++ b/apps/daemon/internal/agent/view_test.go @@ -32,6 +32,9 @@ func TestViewValidate(t *testing.T) { "shim path equal to a mask": func(v *agent.View) { v.ShimPaths = append(v.ShimPaths, "/etc/harness") }, "overlay in the private root": func(v *agent.View) { v.Overlays[1].Path = "/.oac/certs" }, "mask in /proc": func(v *agent.View) { v.Masks[0].Path = "/proc/cpuinfo" }, + "overlay in /sys": func(v *agent.View) { v.Overlays[1].Path = "/sys/fs/cgroup" }, + "mask in /sys": func(v *agent.View) { v.Masks[0].Path = "/sys/devices" }, + "shim in /sys": func(v *agent.View) { v.ShimPaths = append(v.ShimPaths, "/sys/tool") }, "unclean view path": func(v *agent.View) { v.Masks[0].Path = "/etc/../etc/harness" }, "duplicate shim": func(v *agent.View) { v.Shims = append(v.Shims, "git") }, "shim named as the relay": func(v *agent.View) { v.Shims = append(v.Shims, agent.ViewRelayName) }, diff --git a/apps/daemon/internal/agenthost/doc.go b/apps/daemon/internal/agenthost/doc.go index b409761cf..292129aae 100644 --- a/apps/daemon/internal/agenthost/doc.go +++ b/apps/daemon/internal/agenthost/doc.go @@ -88,7 +88,8 @@ // Session home read-write and noexec, the agent host's /etc/passwd, group, // hosts, resolv.conf and nsswitch.conf, the agent host's CA directory at its // host path, then the adapter's overlays and masks and the process shim with -// its relay. Everything else is the world, or nothing in an empty-root view. +// its relay. The view owns /proc, /sys and /dev. Everything else is the +// world, or nothing in an empty-root view. // // The Session directory, StateDir/sessions/, stays root-owned // and private. Its home holds the Harness's native history and persists diff --git a/apps/daemon/internal/agenthost/environment_linux_test.go b/apps/daemon/internal/agenthost/environment_linux_test.go index 2d0c3cd93..9cfe332e9 100644 --- a/apps/daemon/internal/agenthost/environment_linux_test.go +++ b/apps/daemon/internal/agenthost/environment_linux_test.go @@ -556,7 +556,7 @@ func TestEnvironmentOwnerKeepsWorkspaceAcrossRouters(t *testing.T) { t.Fatalf("owner lost at epoch %d", epoch) } } - for _, workspace := range []string{"", "relative", "/projects/../two", "C:/project", "/", "/.oac", "/.oac/home", "/proc/1", "/dev/shm", "/etc", "/etc/passwd", "/trust", "/trust/roots"} { + for _, workspace := range []string{"", "relative", "/projects/../two", "C:/project", "/", "/.oac", "/.oac/home", "/proc/1", "/sys", "/sys/project", "/dev/shm", "/etc", "/etc/passwd", "/trust", "/trust/roots"} { fresh := newBinding(newResource()) invalid := bindPayload(fresh) invalid.WorkspaceDirectory = workspace diff --git a/apps/daemon/internal/sessionview/build_linux.go b/apps/daemon/internal/sessionview/build_linux.go index 30044de1b..1f82a1119 100644 --- a/apps/daemon/internal/sessionview/build_linux.go +++ b/apps/daemon/internal/sessionview/build_linux.go @@ -107,6 +107,27 @@ func (b *builder) build(spec *launchSpec) error { if err := b.attach(b.proc, b.root, at, true); err != nil { return err } + at, err = b.at(agent.ViewSysRoot) + if err != nil { + return err + } + sys, err := newFS("sysfs", nil, unix.MOUNT_ATTR_RDONLY|attrNoSuid|attrNoDev|attrNoExec) + if err != nil { + return err + } + defer unix.Close(sys) + if err := b.attach(sys, b.root, at, true); err != nil { + return err + } + // Mount in the view's cgroup namespace, never bind the host hierarchy. + cgroup, err := newFS("cgroup2", nil, unix.MOUNT_ATTR_RDONLY|attrNoSuid|attrNoDev|attrNoExec) + if err != nil { + return err + } + defer unix.Close(cgroup) + if err := b.attachAt(cgroup, sys, "fs/cgroup", agent.ViewSysRoot+"/fs/cgroup", true); err != nil { + return err + } return b.dev() } diff --git a/apps/daemon/internal/sessionview/doc.go b/apps/daemon/internal/sessionview/doc.go index b075743fd..b47364303 100644 --- a/apps/daemon/internal/sessionview/doc.go +++ b/apps/daemon/internal/sessionview/doc.go @@ -1,6 +1,6 @@ // Package sessionview runs a process, and others spawned beside it, inside a per-Session view on the agent host. // -// A view is a private mount, PID and network namespace whose root is the Session's world: a FUSE file system that the daemon serves over a /dev/fuse connection. The launcher adds the local pieces on top of the world: private directories under /.oac, trusted overlays, the command shim, a fresh /proc and a minimal /dev. The world presents a mountpoint for each piece and reports where, following the sandbox's symlinks, and the launcher mounts at those paths without following any symlink itself. A [Spec] without a world gets an empty root instead: a read-only, noexec tmpfs that holds only the mountpoints, each at its own path. The process starts with no capabilities, no_new_privs, a seccomp filter and only stdin, stdout and stderr open. Its network namespace has only loopback up. +// A view is private mount, PID, network and cgroup namespaces whose root is the Session's world: a FUSE file system that the daemon serves over a /dev/fuse connection. The launcher adds the local pieces on top of the world: private directories under /.oac, trusted overlays, the command shim, fresh /proc and read-only /sys filesystems, a read-only cgroup2 mount rooted at the view and a minimal /dev. The world presents a mountpoint for each piece and reports where, following the sandbox's symlinks, and the launcher mounts at those paths without following any symlink itself. A [Spec] without a world gets an empty root instead: a read-only, noexec tmpfs that holds only the mountpoints, each at its own path. The process starts with no capabilities, no_new_privs, a seccomp filter and only stdin, stdout and stderr open. Its network namespace has only loopback up. // // With a world, the initial process starts only in a directory on that world: the launcher resolves [Process].Dir without symlinks or crossing a mount, then enters the pinned directory before starting the process. Private directories and overlays cannot become its workspace through aliases. An empty-root view and later [View.Spawn] commands retain ordinary working-directory resolution. // diff --git a/apps/daemon/internal/sessionview/view_linux.go b/apps/daemon/internal/sessionview/view_linux.go index 852463b62..83a241b30 100644 --- a/apps/daemon/internal/sessionview/view_linux.go +++ b/apps/daemon/internal/sessionview/view_linux.go @@ -122,6 +122,11 @@ func (v *View) launch(spec *Spec) error { if v.cgroup, err = os.MkdirTemp(spec.CgroupParent, "view-*"); err != nil { return &Error{Kind: ErrCgroup, Op: "create", Path: spec.CgroupParent, Err: err} } + // The view reads its own kernel accounting through a read-only mount. + // Keep root ownership and grant no cgroup management permissions. + if err := os.Chmod(v.cgroup, 0o555); err != nil { + return &Error{Kind: ErrCgroup, Op: "chmod", Path: v.cgroup, Err: err} + } cgroup, err := os.Open(v.cgroup) if err != nil { return &Error{Kind: ErrCgroup, Op: "open", Path: v.cgroup, Err: err} @@ -164,7 +169,7 @@ func (v *View) launch(spec *Spec) error { ExtraFiles: files, // Cloning into the namespaces, rather than unsharing later, puts every runtime thread of the launcher in them and makes it PID 1 of the view. Cloning into the cgroup, rather than moving the launcher there, means that no process of the view ever runs outside it. SysProcAttr: &syscall.SysProcAttr{ - Cloneflags: syscall.CLONE_NEWNS | syscall.CLONE_NEWNET | syscall.CLONE_NEWPID, + Cloneflags: syscall.CLONE_NEWNS | syscall.CLONE_NEWNET | syscall.CLONE_NEWPID | syscall.CLONE_NEWCGROUP, Setsid: true, UseCgroupFD: true, CgroupFD: int(cgroup.Fd()), @@ -703,6 +708,7 @@ func (s *Spec) mountpoints() []Mountpoint { } m = append(m, Mountpoint{Path: agent.ViewProcRoot, Dir: true}, + Mountpoint{Path: agent.ViewSysRoot, Dir: true}, Mountpoint{Path: agent.ViewDevRoot, Dir: true}, ) for _, o := range s.Overlays { diff --git a/apps/daemon/internal/sessionview/view_linux_test.go b/apps/daemon/internal/sessionview/view_linux_test.go index 3e6066be2..36e86939c 100644 --- a/apps/daemon/internal/sessionview/view_linux_test.go +++ b/apps/daemon/internal/sessionview/view_linux_test.go @@ -69,6 +69,11 @@ func TestMain(m *testing.M) { func TestViewIsolation(t *testing.T) { requireView(t) f := newFixture(t) + sibling := filepath.Join(f.cgroups, "other-view") + if err := unix.Mkdir(sibling, 0o755); err != nil { + t.Fatal(err) + } + defer unix.Rmdir(sibling) w := &loopbackWorld{dir: f.world} spec := f.spec(w, "probe", "OAC_VIEW_HOST_PATH="+f.self) spec.Network.Setup = serveBroker(t) @@ -803,6 +808,7 @@ func TestWorldInitialDirectoryStaysInWorld(t *testing.T) { {"private mount", "/.oac/home", unix.EXDEV}, {"overlay mount", "/etc/oac-overlay", unix.EXDEV}, {"proc mount", "/proc", unix.EXDEV}, + {"sys mount", "/sys", unix.EXDEV}, {"custom workspace", "/data/project", nil}, {"world root", "/", nil}, } { @@ -920,7 +926,7 @@ func newFixture(t *testing.T) *fixture { staging: filepath.Join(base, "staging"), cgroups: sessionviewtest.CgroupParent(t), } - for _, d := range []string{".oac/harness", ".oac/home", ".oac/run", ".oac/bin", "proc", "dev", "bin", "usr/bin", "data", "etc/oac-overlay"} { + for _, d := range []string{".oac/harness", ".oac/home", ".oac/run", ".oac/bin", "proc", "sys", "dev", "bin", "usr/bin", "data", "etc/oac-overlay"} { mkdir(t, filepath.Join(f.world, d)) } writeFile(t, filepath.Join(f.world, "bin", "sh"), "") @@ -1363,7 +1369,11 @@ func runHelper(mode string) int { if err := unix.Statfs("/", &st); err != nil || st.Type != unix.TMPFS_MAGIC || st.Flags&(unix.ST_RDONLY|unix.ST_NOEXEC) != unix.ST_RDONLY|unix.ST_NOEXEC { errs = append(errs, fmt.Errorf("root: type %#x flags %#x, %v", st.Type, st.Flags, err)) } - for dir, want := range map[string][]string{"/": {".oac", "dev", "etc", "proc"}, "/.oac": {"bin", "harness", "home"}, "/.oac/bin": nil, "/etc": {"oac-overlay"}} { + if err := systemFilesystems(); err != nil { + fmt.Fprintln(os.Stderr, err) + return 1 + } + for dir, want := range map[string][]string{"/": {".oac", "dev", "etc", "proc", "sys"}, "/.oac": {"bin", "harness", "home"}, "/.oac/bin": nil, "/etc": {"oac-overlay"}} { entries, err := os.ReadDir(dir) var names []string for _, e := range entries { @@ -1516,6 +1526,7 @@ var viewChecks = []struct { } return nil }}, + {"system filesystems belong to the view", systemFilesystems}, {"the relay runs as the view user without privileges", func() error { pid := relayPID() if pid == 0 { @@ -1585,6 +1596,54 @@ func relayPID() int { return 0 } +func systemFilesystems() error { + for path, kind := range map[string]int64{"/sys": unix.SYSFS_MAGIC, "/sys/fs/cgroup": unix.CGROUP2_SUPER_MAGIC} { + var stat unix.Statfs_t + if err := unix.Statfs(path, &stat); err != nil { + return err + } + if stat.Type != kind || stat.Flags&unix.ST_RDONLY == 0 || stat.Flags&unix.ST_NOEXEC == 0 { + return fmt.Errorf("%s is not the read-only, noexec kernel filesystem: type=%x flags=%x", path, stat.Type, stat.Flags) + } + } + if err := fileHas("/proc/self/cgroup", "0::/\n"); err != nil { + return err + } + procs, err := os.ReadFile("/sys/fs/cgroup/cgroup.procs") + if err != nil || !slices.Contains(strings.Fields(string(procs)), strconv.Itoa(os.Getpid())) { + return fmt.Errorf("current cgroup omits self: %q, %v", procs, err) + } + entries, err := os.ReadDir("/sys/fs/cgroup") + if err != nil { + return err + } + for _, entry := range entries { + if entry.IsDir() { + return fmt.Errorf("another cgroup is visible: %s", entry.Name()) + } + } + devices, err := os.ReadDir("/sys/class/net") + if err != nil { + return err + } + for _, device := range devices { + if device.Type()&os.ModeSymlink != 0 && device.Name() != "lo" { + return fmt.Errorf("another network namespace's device is visible: %s", device.Name()) + } + } + for _, path := range []string{"/sys/fs/cgroup/cgroup.procs", "/sys/devices/system/cpu/online"} { + f, err := os.OpenFile(path, os.O_WRONLY, 0) + if f != nil { + f.Close() + return fmt.Errorf("system file is writable: %s", path) + } + if !errors.Is(err, syscall.EROFS) && !errors.Is(err, syscall.EACCES) { + return fmt.Errorf("write system file %s: %v", path, err) + } + } + return nil +} + func onlyStdio() error { entries, err := os.ReadDir("/proc/self/fd") if err != nil { diff --git a/contracts/agents-api/harness-onboarding.md b/contracts/agents-api/harness-onboarding.md index 5251b7ce1..db8863d81 100644 --- a/contracts/agents-api/harness-onboarding.md +++ b/contracts/agents-api/harness-onboarding.md @@ -294,20 +294,22 @@ An agent host runs the Harness outside the sandbox, in a per-Session view. The v - view and host paths are absolute and clean; - closure names are single path components other than `bin`, `home` and `run`, which the agent host uses for the shims, the Session home and the process relay; -- shim paths, overlays and masks do not overlap each other or `/`, and stay out of the trees the view builds itself: `/.oac`, `/proc` and `/dev` (`ViewReserved`); +- shim paths, overlays and masks do not overlap each other or `/`, and stay out of the trees the view builds itself: `/.oac`, `/proc`, `/sys` and `/dev` (`ViewReserved`); - each `LocalExec` entry lies in a closure directory or an `Exec` overlay; - shim names and `ForwardEnv` names are unique, no shim is named `oac-process-shim`, which is the process relay's, or starts with `oac-mcp-`, which [stdio aliases](#stdio-mcp) use, a variable name contains no `=`, and `ForwardEnv` names no variable the view or the broker sets ([Environment](#environment)); - `Proxy` is one of the two values and `Executor` is non-nil. `harness.go` defines the view layout once, and `sessionview` builds views from it. The agent host checks its own overlays, such as `/etc/passwd`, against the declaration when it builds the view. A workspace must remain entirely in the sandbox world: binding rejects overlap with the common reserved trees or agent-host overlays before any Environment effect, and Harness admission rejects overlap with its declared overlays, masks or shim paths before any native effect. Neither operation substitutes a private home or another directory. At initial launch with a sandbox world, the launcher opens the working directory beneath that world without following symlinks or crossing mounts, then enters the opened directory before forking; changing the path cannot redirect startup into a view-owned mount. An empty-root launch and `Spawn` retain their own directory rules, including native-history helpers in the private home. This startup check does not restrict where the native process may later change directory. +The view owns the native process's kernel interfaces: a fresh `/proc`, read-only `/sys` and minimal `/dev`. A fresh read-only cgroup2 mount at `/sys/fs/cgroup` is rooted in the view's cgroup namespace, so `/proc/self/cgroup` and the visible hierarchy describe the same process group without exposing ancestor or sibling cgroups. The view mounts sysfs in its own network namespace; it never binds the host's `/sys` tree. Native file tools see these reserved kernel paths locally. Commands forwarded through the Process protocol and public File operations continue to use the sandbox's filesystem, including its `/sys`; workspace files remain in the sandbox world. These mounts also exist in an empty-root view. + ### Capabilities A view runs every request that the kind's declaration admits, so the adapter declares only what its view runs, and dispatch checks each request against that declaration. The agent host serves a local Environment and environment none, and every view runs the Environment's installed Skills and [stdio MCP](#stdio-mcp). The Environment owner fills `PrepareRequest.Skills` and `CapabilityRoot` as sandbox paths, and the adapter hands them to its Harness; only the Harness reads them, through the view, and the adapter opens none of them on the agent host. The agent host rejects a stdio binding that needs a credential with `ErrViewHandoff`. ### Environment none -A request with `DisableExecutionEnvironment` runs in an empty-root view: a read-only, noexec tmpfs at `/` that holds only the mountpoints for the closure, the Session home, the agent host's runtime files, `/proc`, `/dev` and the overlays. It has no sandbox files, no shims, no Link attachment and no sandbox network, so the generic proxy refuses every request; the cgroup, the isolation and the gateway stay. The request carries no `LocalEnvironment`, and the Harness runs in `/.oac/home/work` (`ViewWorkName`). The request already expresses the profile, so the wire has no field for it. A request with neither `LocalEnvironment` nor `DisableExecutionEnvironment` is an incomplete binding, and the agent host rejects it. +A request with `DisableExecutionEnvironment` runs in an empty-root view: a read-only, noexec tmpfs at `/` that holds only the mountpoints for the closure, the Session home, the agent host's runtime files, `/proc`, `/sys`, `/dev` and the overlays. It has no sandbox files, no shims, no Link attachment and no sandbox network, so the generic proxy refuses every request; the cgroup, the isolation and the gateway stay. The request carries no `LocalEnvironment`, and the Harness runs in `/.oac/home/work` (`ViewWorkName`). The request already expresses the profile, so the wire has no field for it. A request with neither `LocalEnvironment` nor `DisableExecutionEnvironment` is an incomplete binding, and the agent host rejects it. ### Executables diff --git a/contracts/agents-api/zh/harness-onboarding.md b/contracts/agents-api/zh/harness-onboarding.md index 005bd3e66..efe992c38 100644 --- a/contracts/agents-api/zh/harness-onboarding.md +++ b/contracts/agents-api/zh/harness-onboarding.md @@ -1,7 +1,7 @@ --- title: "添加 Harness" source: contracts/agents-api/harness-onboarding.md -source_hash: a18a2221b28aa01596574a1db63e1bf643dc43fb73c15261fb8932dd447f499b +source_hash: e69944fa75e85c1154c8ef5921195a84a065c95f2d2d99c91fcf6024651c3634 --- **Harness** 是一种运行模型和工具循环的原生代理引擎(Codex、Claude Code、MiniMax Code)。**Harness 适配器**将 Runtime 的 Executor 和 Turn 契约转换到该引擎的 SDK 或协议。本文档定义 Runtime–Harness 协议:适配器接口及其生命周期义务、注册、支持声明和验收。 @@ -296,20 +296,22 @@ agent host 在沙箱之外、在每个 Session 一个的视图中运行 Harness - 视图路径和主机路径都是干净的绝对路径; - closure 名称是单个路径分量,且不是 `bin`、`home` 和 `run`,这三个由 agent host 用于 shim、Session home 和进程 relay; -- shim 路径、overlay 和 mask 互不重叠,也不与 `/` 重叠,并且不进入视图自己构建的树:`/.oac`、`/proc` 和 `/dev`(`ViewReserved`); +- shim 路径、overlay 和 mask 互不重叠,也不与 `/` 重叠,并且不进入视图自己构建的树:`/.oac`、`/proc`、`/sys` 和 `/dev`(`ViewReserved`); - 每个 `LocalExec` 条目都位于某个 closure 目录或某个 `Exec` overlay 中; - shim 名称和 `ForwardEnv` 名称各自唯一,没有 shim 名为 `oac-process-shim`(该名称属于进程 relay)或以 `oac-mcp-` 开头(该前缀属于 [stdio 别名](#stdio-mcp)),变量名不含 `=`,且 `ForwardEnv` 不指定视图或 broker 设置的变量([环境](#environment)); - `Proxy` 是两个取值之一,且 `Executor` 非 nil。 `harness.go` 只定义一次视图布局,`sessionview` 据此构建视图。agent host 在构建视图时,用声明检查它自己的 overlay,例如 `/etc/passwd`。工作区必须完整地位于沙箱世界中:绑定会在任何 Environment 副作用之前拒绝与公共保留树或 agent-host overlay 的重叠;Harness 准入会在任何原生副作用之前拒绝与其声明的 overlay、mask 或 shim 路径的重叠。这两种操作都不会改用私有 home 或其他目录。带沙箱世界的初次启动中,launcher 会在该世界内打开工作目录,不跟随符号链接也不跨越挂载点,然后在 fork 前进入已打开的目录;路径被修改也不能将启动重定向到视图所属挂载点。空根启动和 `Spawn` 保留各自的目录规则,包括在私有 home 中运行原生历史 helper。此启动检查不会限制原生进程随后切换目录的位置。 +视图拥有原生进程的内核接口:新挂载的 `/proc`、只读 `/sys` 和最小 `/dev`。`/sys/fs/cgroup` 上新挂载的只读 cgroup2 以视图的 cgroup 命名空间为根,因此 `/proc/self/cgroup` 与可见层级描述相同的进程组,不暴露祖先或兄弟 cgroup。视图在自己的网络命名空间中挂载 sysfs,绝不绑定宿主的 `/sys` 树。原生文件工具在本地读取这些保留的内核路径。通过 Process 协议转发的命令和公共 File 操作仍使用沙箱文件系统,包括沙箱的 `/sys`;工作区文件仍位于沙箱世界中。这些挂载也存在于空根视图中。 + ### 能力 {#capabilities} 视图运行该 kind 的声明所准入的每个请求,因此 adapter 只声明其视图能运行的内容,dispatch 按该声明检查每个请求。agent host 提供本地 Environment 和 environment none,且每个视图都运行 Environment 已安装的 Skills 和 [stdio MCP](#stdio-mcp)。Environment owner 以沙箱路径填写 `PrepareRequest.Skills` 和 `CapabilityRoot`,adapter 把它们交给 Harness;只有 Harness 通过视图读取它们,adapter 不在 agent host 上打开其中任何路径。agent host 以 `ErrViewHandoff` 拒绝需要凭据的 stdio 绑定。 ### Environment none {#environment-none} -设置了 `DisableExecutionEnvironment` 的请求在空根视图中运行:`/` 是只读、noexec 的 tmpfs,只包含 closure、Session home、agent host 运行时文件、`/proc`、`/dev` 和 overlay 的挂载点。它没有沙箱文件、没有 shim、没有 Link 附着,也没有沙箱网络,因此通用代理拒绝每个请求;cgroup、隔离和网关保持不变。请求不携带 `LocalEnvironment`,Harness 在 `/.oac/home/work`(`ViewWorkName`)中运行。请求本身已经表达了这一配置,因此线协议没有对应字段。既没有 `LocalEnvironment` 也没有 `DisableExecutionEnvironment` 的请求是不完整的绑定,agent host 会拒绝它。 +设置了 `DisableExecutionEnvironment` 的请求在空根视图中运行:`/` 是只读、noexec 的 tmpfs,只包含 closure、Session home、agent host 运行时文件、`/proc`、`/sys`、`/dev` 和 overlay 的挂载点。它没有沙箱文件、没有 shim、没有 Link 附着,也没有沙箱网络,因此通用代理拒绝每个请求;cgroup、隔离和网关保持不变。请求不携带 `LocalEnvironment`,Harness 在 `/.oac/home/work`(`ViewWorkName`)中运行。请求本身已经表达了这一配置,因此线协议没有对应字段。既没有 `LocalEnvironment` 也没有 `DisableExecutionEnvironment` 的请求是不完整的绑定,agent host 会拒绝它。 ### 可执行文件 {#executables}