From dd80974be12129e016a96ad3f0df6f5dff645a7f Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Fri, 9 Oct 2026 15:51:20 +0000 Subject: [PATCH] Use local system trust for native Harnesses --- apps/daemon/internal/agenthost/admit.go | 41 ++++--- .../internal/agenthost/admit_linux_test.go | 100 +++++++++++++++++- apps/daemon/internal/agenthost/agenthost.go | 8 +- .../agenthost/agenthost_linux_test.go | 4 +- apps/daemon/internal/agenthost/doc.go | 2 +- .../agenthost/environment_linux_test.go | 2 +- .../daemon/internal/agenthost/launch_linux.go | 4 +- .../internal/agenthost/view_linux_test.go | 11 +- .../agenthostqualify/qualify_linux_test.go | 6 +- .../agenthostqualify/rig_linux_test.go | 2 +- apps/daemon/internal/cli/agent_host_linux.go | 4 +- .../internal/cli/agent_host_linux_test.go | 4 +- contracts/agents-api/harness-onboarding.md | 2 + contracts/agents-api/zh/harness-onboarding.md | 4 +- 14 files changed, 147 insertions(+), 47 deletions(-) diff --git a/apps/daemon/internal/agenthost/admit.go b/apps/daemon/internal/agenthost/admit.go index 32f6a25e7..a83ba853d 100644 --- a/apps/daemon/internal/agenthost/admit.go +++ b/apps/daemon/internal/agenthost/admit.go @@ -39,7 +39,7 @@ type plan struct { executables processbroker.Executables } -// checkConfig validates cfg and loads the roots in its CA directory. +// checkConfig validates cfg and loads the roots in its CA bundle. func checkConfig(cfg Config) (*x509.CertPool, error) { switch { case !isHostPath(cfg.StateDir): @@ -56,34 +56,29 @@ func checkConfig(cfg Config) (*x509.CertPool, error) { return nil, invalidConfig("no Harness declarations") case !isHostPath(cfg.Shim): return nil, invalidConfig("shim %q is not absolute and clean", cfg.Shim) - case !isHostPath(cfg.CADir) || cfg.CADir == "/" || agent.ViewReserved(cfg.CADir): - return nil, invalidConfig("CA directory %q", cfg.CADir) + case !isHostPath(cfg.CAFile) || cfg.CAFile == "/" || agent.ViewReserved(cfg.CAFile): + return nil, invalidConfig("CA file %q", cfg.CAFile) } - return loadRoots(cfg.CADir) + return loadRoots(cfg.CAFile) } -// loadRoots reads every certificate in dir. Each entry is a regular file of -// PEM certificates, so the view presents exactly what the gateway trusts. -func loadRoots(dir string) (*x509.CertPool, error) { - entries, err := os.ReadDir(dir) +// loadRoots reads the bundle presented in the view, so the gateway uses the +// same trust source without falling back to another root store. +func loadRoots(name string) (*x509.CertPool, error) { + info, err := os.Lstat(name) if err != nil { - return nil, fmt.Errorf("%w: CA directory: %w", ErrInvalidConfig, err) + return nil, fmt.Errorf("%w: CA file: %w", ErrInvalidConfig, err) } - if len(entries) == 0 { - return nil, invalidConfig("CA directory %s is empty", dir) + if !info.Mode().IsRegular() { + return nil, invalidConfig("CA file %s is not a regular file", name) + } + data, err := os.ReadFile(name) + if err != nil { + return nil, fmt.Errorf("%w: CA file: %w", ErrInvalidConfig, err) } roots := x509.NewCertPool() - for _, e := range entries { - if !e.Type().IsRegular() { - return nil, invalidConfig("CA entry %s is not a regular file", e.Name()) - } - data, err := os.ReadFile(filepath.Join(dir, e.Name())) - if err != nil { - return nil, fmt.Errorf("%w: CA directory: %w", ErrInvalidConfig, err) - } - if !roots.AppendCertsFromPEM(data) { - return nil, invalidConfig("CA entry %s holds no PEM certificate", e.Name()) - } + if !roots.AppendCertsFromPEM(data) { + return nil, invalidConfig("CA file %s holds no PEM certificate", name) } return roots, nil } @@ -178,7 +173,7 @@ func checkLayout(cfg Config, view agent.View, workspace string) error { if workspace == "/" || agent.ViewReserved(workspace) { return unsupported("workspace overlaps a reserved view tree") } - own := []string{cfg.CADir} + own := []string{cfg.CAFile} for _, name := range etcFiles { own = append(own, "/etc/"+name) } diff --git a/apps/daemon/internal/agenthost/admit_linux_test.go b/apps/daemon/internal/agenthost/admit_linux_test.go index 17f1a617d..43f2d51f1 100644 --- a/apps/daemon/internal/agenthost/admit_linux_test.go +++ b/apps/daemon/internal/agenthost/admit_linux_test.go @@ -4,6 +4,11 @@ package agenthost import ( "context" + "crypto/ed25519" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "encoding/pem" "errors" "net/http" "net/http/httptest" @@ -14,6 +19,7 @@ import ( "strings" "sync/atomic" "testing" + "time" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/processbroker" @@ -85,7 +91,9 @@ func TestAdmissionRejectsBeforeAnyEffect(t *testing.T) { "relative workspace": {"viewed", func(r *agent.PrepareRequest) { r.WorkspaceRoot = "workspace" }, []error{ErrInvalidSession}}, "private workspace": {"viewed", func(r *agent.PrepareRequest) { r.WorkspaceRoot = "/.oac/home" }, unsupported}, "control workspace": {"viewed", func(r *agent.PrepareRequest) { r.WorkspaceRoot = "/proc" }, unsupported}, - "host CA workspace": {"viewed", func(r *agent.PrepareRequest) { r.WorkspaceRoot = f.cfg.CADir }, unsupported}, + "host CA workspace": {"viewed", func(r *agent.PrepareRequest) { r.WorkspaceRoot = f.cfg.CAFile }, unsupported}, + "host CA workspace ancestor": {"viewed", func(r *agent.PrepareRequest) { r.WorkspaceRoot = filepath.Dir(f.cfg.CAFile) }, unsupported}, + "host CA workspace child": {"viewed", func(r *agent.PrepareRequest) { r.WorkspaceRoot = f.cfg.CAFile + "/project" }, unsupported}, "host overlay ancestor": {"viewed", func(r *agent.PrepareRequest) { r.WorkspaceRoot = "/etc" }, unsupported}, "masked workspace": {"covered", func(r *agent.PrepareRequest) { r.WorkspaceRoot = "/masked" }, unsupported}, "masked workspace child": {"covered", func(r *agent.PrepareRequest) { r.WorkspaceRoot = "/masked/project" }, unsupported}, @@ -247,3 +255,93 @@ func TestReleaseRemovesTheHome(t *testing.T) { t.Errorf("the Session directory remains: %v", err) } } + +func TestCABundleRejectsInvalidFiles(t *testing.T) { + cfg := newConfig(t, agent.NewRegistry(), testCA()) + link := filepath.Join(t.TempDir(), "roots.pem") + if err := os.Symlink(cfg.CAFile, link); err != nil { + t.Fatal(err) + } + for _, name := range []string{"", "relative", "/", "/.oac/roots.pem", cfg.CAFile + ".missing", filepath.Dir(cfg.CAFile), link} { + bad := cfg + bad.CAFile = name + if _, err := checkConfig(bad); !errors.Is(err, ErrInvalidConfig) { + t.Errorf("CAFile %q: %v", name, err) + } + } + for _, data := range [][]byte{nil, []byte("not PEM"), pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: []byte("not DER")})} { + if err := os.WriteFile(cfg.CAFile, data, 0o644); err != nil { + t.Fatal(err) + } + if _, err := checkConfig(cfg); !errors.Is(err, ErrInvalidConfig) { + t.Errorf("invalid bundle: %v", err) + } + } +} + +func TestGatewayUsesOnlyTheCABundleRoots(t *testing.T) { + upstream := httptest.NewTLSServer(http.NotFoundHandler()) + defer upstream.Close() + // A separate valid issuer must not fall back to the upstream's issuer. + pub, key, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + template := x509.Certificate{ + SerialNumber: upstream.Certificate().SerialNumber, + NotBefore: upstream.Certificate().NotBefore, NotAfter: upstream.Certificate().NotAfter, + IsCA: true, BasicConstraintsValid: true, KeyUsage: x509.KeyUsageCertSign, + } + der, err := x509.CreateCertificate(rand.Reader, &template, &template, pub, key) + if err != nil { + t.Fatal(err) + } + other, err := x509.ParseCertificate(der) + if err != nil { + t.Fatal(err) + } + for _, tc := range []struct { + name string + ca *x509.Certificate + trusted bool + }{ + {"trusted", upstream.Certificate(), true}, {"untrusted", other, false}, + } { + t.Run(tc.name, func(t *testing.T) { + f := newViewFixture(t) + cfg := newConfig(t, f.cfg.Harnesses, tc.ca) + roots, err := checkConfig(cfg) + if err != nil { + t.Fatal(err) + } + p, err := admit(cfg, roots, prepared(request("viewed", upstream.URL, "fixture-key")), Environment{}, nil) + if err != nil { + t.Fatal(err) + } + transport := &http.Transport{TLSClientConfig: &tls.Config{RootCAs: p.gateway.RootCAs}} + defer transport.CloseIdleConnections() + client := &http.Client{Transport: transport, Timeout: 5 * time.Second} + response, err := client.Get(upstream.URL) + if response != nil { + response.Body.Close() + } + if tc.trusted && err != nil { + t.Fatalf("trusted TLS: %v", err) + } + var unknown x509.UnknownAuthorityError + if !tc.trusted && !errors.As(err, &unknown) { + t.Fatalf("untrusted TLS: %v, want unknown authority", err) + } + }) + } +} + +func TestHarnessCannotCoverCABundle(t *testing.T) { + f := newViewFixture(t) + for _, target := range []string{f.cfg.CAFile, filepath.Dir(f.cfg.CAFile), f.cfg.CAFile + "/child"} { + view := agent.View{Overlays: []agent.ViewOverlay{{Path: target, Source: t.TempDir()}}} + if err := checkLayout(f.cfg, view, ""); !errors.Is(err, agent.ErrInvalidView) { + t.Errorf("overlay %s: %v", target, err) + } + } +} diff --git a/apps/daemon/internal/agenthost/agenthost.go b/apps/daemon/internal/agenthost/agenthost.go index ddd15066f..e16201246 100644 --- a/apps/daemon/internal/agenthost/agenthost.go +++ b/apps/daemon/internal/agenthost/agenthost.go @@ -39,10 +39,10 @@ type Config struct { Harnesses *agent.Registry // Shim is the absolute host path of the static oac-process-shim binary. Shim string - // CADir is an absolute host directory of regular PEM files: the roots the - // agent host trusts. The gateway trusts exactly these for upstream TLS, - // and the view presents the directory read-only at the same path. - CADir string + // CAFile is an absolute host path to a regular PEM CA bundle. The gateway + // trusts these roots for upstream TLS, and the view presents the same file + // read-only at the same path. + CAFile string // Log receives each view's presentation report and each failure of a // Session, which no Turn reports. Nil discards it. Log *slog.Logger diff --git a/apps/daemon/internal/agenthost/agenthost_linux_test.go b/apps/daemon/internal/agenthost/agenthost_linux_test.go index 48b696cf1..c3edbc198 100644 --- a/apps/daemon/internal/agenthost/agenthost_linux_test.go +++ b/apps/daemon/internal/agenthost/agenthost_linux_test.go @@ -51,7 +51,7 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } -// newConfig returns a Config whose CA directory holds ca. Its ViewCgroups is +// newConfig returns a Config whose CA bundle holds ca. Its ViewCgroups is // a plain directory, which Open rejects. func newConfig(t *testing.T, reg *agent.Registry, ca *x509.Certificate) Config { t.Helper() @@ -67,7 +67,7 @@ func newConfig(t *testing.T, reg *agent.Registry, ca *x509.Certificate) Config { t.Fatal(err) } return Config{StateDir: t.TempDir(), UIDs: UIDRange{First: 70000, Count: 8}, ViewCgroups: t.TempDir(), RelayURL: "ws://127.0.0.1:9", - RuntimeID: sandboxwire.NewID(), Credential: []byte("runtime-credential"), Harnesses: reg, Shim: exe, CADir: dir} + RuntimeID: sandboxwire.NewID(), Credential: []byte("runtime-credential"), Harnesses: reg, Shim: exe, CAFile: filepath.Join(dir, "ca.pem")} } // register declares kind with view, or without one when view is nil, as diff --git a/apps/daemon/internal/agenthost/doc.go b/apps/daemon/internal/agenthost/doc.go index 292129aae..c5f773ca8 100644 --- a/apps/daemon/internal/agenthost/doc.go +++ b/apps/daemon/internal/agenthost/doc.go @@ -86,7 +86,7 @@ // // Each view presents the closure directories read-only and executable, the // 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 +// hosts, resolv.conf and nsswitch.conf, the agent host's CA bundle read-only at its // host path, then the adapter's overlays and masks and the process shim with // its relay. The view owns /proc, /sys and /dev. Everything else is the // world, or nothing in an empty-root view. diff --git a/apps/daemon/internal/agenthost/environment_linux_test.go b/apps/daemon/internal/agenthost/environment_linux_test.go index 9cfe332e9..68c01d7bc 100644 --- a/apps/daemon/internal/agenthost/environment_linux_test.go +++ b/apps/daemon/internal/agenthost/environment_linux_test.go @@ -536,7 +536,7 @@ func (s *probed) Write(b []byte) (int, error) { func TestEnvironmentOwnerKeepsWorkspaceAcrossRouters(t *testing.T) { var dials atomic.Int32 - h := &Host{cfg: Config{CADir: "/trust"}, owners: owners{d: deps{dial: countingDial(&dials)}}} + h := &Host{cfg: Config{CAFile: "/trust"}, owners: owners{d: deps{dial: countingDial(&dials)}}} b := newBinding(newResource()) payload := bindPayload(b) payload.WorkspaceDirectory = "/projects/one" diff --git a/apps/daemon/internal/agenthost/launch_linux.go b/apps/daemon/internal/agenthost/launch_linux.go index 1f1ebe7b3..e2785e87f 100644 --- a/apps/daemon/internal/agenthost/launch_linux.go +++ b/apps/daemon/internal/agenthost/launch_linux.go @@ -411,7 +411,7 @@ func (h *ownedView) end(waitErr error) { } // spec builds the view over world: the closure and home directories, the -// agent host's /etc files and CA directory, the adapter's overlays and masks, +// agent host's /etc files and CA bundle, the adapter's overlays and masks, // and the shim under each name and path of the process broker's table. func (s *session) spec(world sessionview.World, opts clirunner.StartOptions, stdio [3]*os.File) sessionview.Spec { view, x := s.plan.view, s.plan.executables @@ -424,7 +424,7 @@ func (s *session) spec(world sessionview.World, opts clirunner.StartOptions, std for _, name := range etcFiles { overlays = append(overlays, sessionview.Overlay{Path: "/etc/" + name, Source: s.dir.entry(etcEntry, name)}) } - overlays = append(overlays, sessionview.Overlay{Path: s.cfg.CADir, Source: s.cfg.CADir}) + overlays = append(overlays, sessionview.Overlay{Path: s.cfg.CAFile, Source: s.cfg.CAFile}) for _, o := range view.Overlays { overlays = append(overlays, sessionview.Overlay{Path: o.Path, Source: o.Source, Exec: o.Exec}) } diff --git a/apps/daemon/internal/agenthost/view_linux_test.go b/apps/daemon/internal/agenthost/view_linux_test.go index 1c2869061..0e6017108 100644 --- a/apps/daemon/internal/agenthost/view_linux_test.go +++ b/apps/daemon/internal/agenthost/view_linux_test.go @@ -109,7 +109,7 @@ func TestSessionRunsInAViewOverItsAttachment(t *testing.T) { Proxy: agent.ViewProxyEnv, Executor: func(_ context.Context, req agent.PrepareRequest, s agent.ViewSession) (agent.Executor, error) { e := &testExecutor{session: s, dir: workDir, - env: []string{harnessEnv + "=1", modelEnv + "=" + req.Prepared.Provider.BaseURL, caEnv + "=" + cfg.CADir, proxyEnv + "=" + s.Proxy}} + env: []string{harnessEnv + "=1", modelEnv + "=" + req.Prepared.Provider.BaseURL, caEnv + "=" + cfg.CAFile, proxyEnv + "=" + s.Proxy}} if req.LocalEnvironment != nil { e.dir = req.WorkspaceRoot } @@ -759,7 +759,7 @@ func (t *testTurn) AwaitSettlement(ctx context.Context) (agent.TurnSettlement, e } } -var harnessChecks = []string{"world rename", "model through the gateway", "no direct route", "world is noexec", "masks", "home", "passwd", "CA directory"} +var harnessChecks = []string{"world rename", "model through the gateway", "no direct route", "world is noexec", "masks", "home", "passwd", "CA bundle"} // workDir is where the Harness of an empty-root view runs. const workDir = agent.ViewPrivateRoot + "/" + agent.ViewHomeName + "/" + agent.ViewWorkName @@ -836,11 +836,14 @@ func runHarness(args []string) int { } return nil }, - "CA directory": func() error { - data, err := os.ReadFile(filepath.Join(os.Getenv(caEnv), "ca.pem")) + "CA bundle": func() error { + data, err := os.ReadFile(os.Getenv(caEnv)) if block, _ := pem.Decode(data); err != nil || block == nil { return fmt.Errorf("no CA certificate: %v", err) } + if err := os.WriteFile(os.Getenv(caEnv), data, 0o644); !errors.Is(err, syscall.EROFS) { + return fmt.Errorf("CA bundle write: %v, want read-only filesystem", err) + } return nil }, } diff --git a/apps/daemon/internal/agenthostqualify/qualify_linux_test.go b/apps/daemon/internal/agenthostqualify/qualify_linux_test.go index d9f6e474b..c239968c9 100644 --- a/apps/daemon/internal/agenthostqualify/qualify_linux_test.go +++ b/apps/daemon/internal/agenthostqualify/qualify_linux_test.go @@ -65,8 +65,8 @@ const ( keyEnv = "OAC_QUALIFY_KEY_FILE" runDir = "/run/qualify" shim = "/opt/oac/bin/oac-process-shim" - // caDir holds the agent-host image's roots, one regular PEM file each. - caDir = "/usr/share/ca-certificates/mozilla" + // caFile holds the agent-host image's system CA bundle. + caFile = "/etc/ssl/certs/ca-certificates.crt" // workspace is the sandbox directory every Session works in. workspace = "/workspace/custom-project" turnLimit = 10 * time.Minute @@ -95,7 +95,7 @@ func TestHarnessSessionsAgainstTheSandbox(t *testing.T) { sb := startSandbox(t) reg := agent.NewRegistry() cfg := agenthost.Config{StateDir: t.TempDir(), ViewCgroups: sessionviewtest.CgroupParent(t), UIDs: agenthost.UIDRange{First: 70000, Count: 8}, RelayURL: sb.url, TLS: sb.tls, - RuntimeID: sandboxwire.NewID(), Credential: []byte("runtime-credential"), Harnesses: reg, Shim: shim, CADir: caDir, + RuntimeID: sandboxwire.NewID(), Credential: []byte("runtime-credential"), Harnesses: reg, Shim: shim, CAFile: caFile, Log: slog.New(slog.NewTextHandler(os.Stderr, nil))} sb.auth.AddRuntime(cfg.Credential, cfg.RuntimeID) sb.ready(t, cfg) diff --git a/apps/daemon/internal/agenthostqualify/rig_linux_test.go b/apps/daemon/internal/agenthostqualify/rig_linux_test.go index 285cb7036..d11a52ca4 100644 --- a/apps/daemon/internal/agenthostqualify/rig_linux_test.go +++ b/apps/daemon/internal/agenthostqualify/rig_linux_test.go @@ -55,7 +55,7 @@ func TestViewCgroupDelegation(t *testing.T) { t.Skipf("set %s=1 and run the test with scripts/qualify-agent-host.sh", gateEnv) } cfg := agenthost.Config{StateDir: t.TempDir(), ViewCgroups: "/sys/fs/cgroup", UIDs: agenthost.UIDRange{First: 70000, Count: 8}, RelayURL: "ws://127.0.0.1:1", - RuntimeID: sandboxwire.NewID(), Credential: []byte("runtime-credential"), Harnesses: agent.NewRegistry(), Shim: shim, CADir: caDir} + RuntimeID: sandboxwire.NewID(), Credential: []byte("runtime-credential"), Harnesses: agent.NewRegistry(), Shim: shim, CAFile: caFile} if h, err := agenthost.Open(cfg); err == nil { h.Close() t.Fatalf("Open accepted the undelegated %s", cfg.ViewCgroups) diff --git a/apps/daemon/internal/cli/agent_host_linux.go b/apps/daemon/internal/cli/agent_host_linux.go index c7aae8327..cfa28c302 100644 --- a/apps/daemon/internal/cli/agent_host_linux.go +++ b/apps/daemon/internal/cli/agent_host_linux.go @@ -33,7 +33,7 @@ import ( const ( agentHostManifest = "/opt/oac/harnesses.json" agentHostShim = "/opt/oac/bin/oac-process-shim" - agentHostCADir = "/usr/share/ca-certificates/mozilla" + agentHostCAFile = "/etc/ssl/certs/ca-certificates.crt" // agentHostState keeps each Session's home across restarts. agentHostState = "/var/lib/oac/agent-host" // agentHostCgroup is where the agent host mounts the container's own @@ -127,7 +127,7 @@ func serveAgentHost(parent context.Context, rc *runContext, args []string, decla } host, err := agenthost.Open(agenthost.Config{StateDir: agentHostState, UIDs: agentHostUIDs, ViewCgroups: views, RelayURL: wsOrigin + "/api/v1/sandbox-link", RuntimeID: sandboxwire.ID(runtimeID), Credential: []byte(identity.Credential), - Harnesses: harnesses, Shim: agentHostShim, CADir: agentHostCADir, Log: obslog.Bg()}) + Harnesses: harnesses, Shim: agentHostShim, CAFile: agentHostCAFile, Log: obslog.Bg()}) if err != nil { return fmt.Errorf("agent-host: %w", err) } diff --git a/apps/daemon/internal/cli/agent_host_linux_test.go b/apps/daemon/internal/cli/agent_host_linux_test.go index 6060f163b..327b522c7 100644 --- a/apps/daemon/internal/cli/agent_host_linux_test.go +++ b/apps/daemon/internal/cli/agent_host_linux_test.go @@ -40,8 +40,8 @@ func TestAgentHostReportsItsDeclarations(t *testing.T) { issuer := httptest.NewTLSServer(nil) issuer.Close() for path, content := range map[string][]byte{ - agentHostManifest: []byte(`{"node": "/usr/local/bin/node", "harnesses": {}}`), - filepath.Join(agentHostCADir, "ca.crt"): pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: issuer.Certificate().Raw}), + agentHostManifest: []byte(`{"node": "/usr/local/bin/node", "harnesses": {}}`), + agentHostCAFile: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: issuer.Certificate().Raw}), } { if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatal(err) diff --git a/contracts/agents-api/harness-onboarding.md b/contracts/agents-api/harness-onboarding.md index 16a2a50c7..5978104bd 100644 --- a/contracts/agents-api/harness-onboarding.md +++ b/contracts/agents-api/harness-onboarding.md @@ -305,6 +305,8 @@ An agent host runs the Harness outside the sandbox, in a per-Session view. The v 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. +The agent host uses the image's system CA bundle at `/etc/ssl/certs/ca-certificates.crt` for the credential gateway's upstream TLS verification and presents that same file read-only at the same view path. Native libraries that discover this bundle read it locally. The agent host does not override a Harness's bundled trust store or change the CA files used by tools running in the sandbox. + ### 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`. diff --git a/contracts/agents-api/zh/harness-onboarding.md b/contracts/agents-api/zh/harness-onboarding.md index d1ef2a330..a94b5faa6 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: 699a60a843ec11e87474212b661dd03231d660d7b3bca0b39d34b979be93db49 +source_hash: f98c9ba9ff366f46dc2cb567c4b5e6a4f3538f59b769c7554bac40a2fa1160e3 --- **Harness** 是一种运行模型和工具循环的原生代理引擎(Codex、Claude Code、MiniMax Code)。**Harness 适配器**将 Runtime 的 Executor 和 Turn 契约转换到该引擎的 SDK 或协议。本文档定义 Runtime–Harness 协议:适配器接口及其生命周期义务、注册、支持声明和验收。 @@ -307,6 +307,8 @@ agent host 在沙箱之外、在每个 Session 一个的视图中运行 Harness 视图拥有原生进程的内核接口:新挂载的 `/proc`、只读 `/sys` 和最小 `/dev`。`/sys/fs/cgroup` 上新挂载的只读 cgroup2 以视图的 cgroup 命名空间为根,因此 `/proc/self/cgroup` 与可见层级描述相同的进程组,不暴露祖先或兄弟 cgroup。视图在自己的网络命名空间中挂载 sysfs,绝不绑定宿主的 `/sys` 树。原生文件工具在本地读取这些保留的内核路径。通过 Process 协议转发的命令和公共 File 操作仍使用沙箱文件系统,包括沙箱的 `/sys`;工作区文件仍位于沙箱世界中。这些挂载也存在于空根视图中。 +agent host 使用镜像的系统 CA bundle `/etc/ssl/certs/ca-certificates.crt` 验证凭据网关的上游 TLS,并将同一文件以只读方式呈现在视图的同一路径。发现此 bundle 的原生库会在本地读取它。agent host 不会覆盖 Harness 自带的信任库,也不会改变沙箱中运行的工具使用的 CA 文件。 + ### 能力 {#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 绑定。