Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 41 additions & 13 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,21 +283,37 @@ unqualified. Skills API references, generic Plugins and capability-directory
imports remain separate work; an adapter-owned Claude plugin envelope does not
implement public Plugins.

Name, enabled/disabled network, initial files, inline Skills and env/setup/system/npm/Python are
Name, enabled/disabled/exact-domain restricted network, initial files, inline Skills and env/setup/system/npm/Python are
implemented independently of remaining installation fields. Reject unsupported
inputs rather than persisting them for silent
omission; expand inline and template initialization together in separately qualified
batches. Resource reads need only tenant authorization, not a live Runtime.
See the [Template coverage and unresolved semantics](contracts/agents-api/environment-templates.md).

SandboxProvider has five operations: Create, GetInfo, Renew, Kill and RunCommand.
Use maintained provider SDKs and thin adapters, Docker first and E2B after the MVP.
Use maintained provider SDKs and thin adapters. The current hosted offering uses Docker.
Provider initialization creates the sandbox and starts its daemon/harness;
RunCommand is for initialization only. Daily execution and Files use Runtime and
native or bounded local capabilities. Docker's lack of a native renewable lease
does not remove service-owned hosted expiry and cleanup requirements.

The E2B Provider uses an explicit `templateID:build_UUID` and the same qualified
The official `openai_hosted` discriminator means hosting by this independent Core
service, using Docker V1. Keep the public value unchanged; `parsar_hosted` is not
a new API type. Public Environment Templates apply only to this hosted path.
E2B onboarding follows the official `self_hosted` workflow: an application or
webhook controller owns sandbox provisioning and cleanup, and the executor connects
with the returned Environment ID, unchanged `remote_url` and scoped environment
authorization. Reuse existing Runtime and provider components without a separate
public integration design. A private daemon connection alone is not evidence of
official interoperability. Qualify tenant ownership, credentials and connection
lifecycle using the pinned client and actual execution.

The previously accepted Core-managed E2B route remains implementation evidence
pending bounded realignment and obsolete-route cleanup after Environment Templates.
Do not expand it as a second hosted offering. Current Template acceptance uses
Docker; historical E2B tests retain only their demonstrated scope.

The existing E2B Provider uses an explicit `templateID:build_UUID` and the same qualified
colocated Runtime. Its root-private bootstrap input and final atomic receipt live
on persistent disk, never template `/run`. Running compute alone does not establish
completed initialization. Inspect exact installation/tenant/Environment/allocation
Expand Down Expand Up @@ -407,14 +423,26 @@ The [co-location qualification inputs](services/agents-api/deploy/codex/README.m
record the pinned native/Docker prerequisites and limits; this switch alone does
not admit hosted Environments or authorize a workspace.

A managed Runtime's enabled/disabled network policy is immutable deployment input,
transferred through the provider-neutral bootstrap and checked against execution
preparation. The native adapter selects the corresponding managed profile; Core
and Docker do not select native profile names. New policy-aware peers advertise
`local_environment_network_policy`; enabled execution requires that capability.
The older explicit-disabled internal peer path remains supported without widening
its policy. Read-only workspace access does not require execution network policy.
A declaration alone does not qualify an image or admit public hosted creation.
A managed Runtime's network policy is immutable deployment input, transferred
through the provider-neutral bootstrap and checked against execution preparation.
The shared policy includes enabled, disabled and an exact-host restricted allowlist.
Core preserves public spelling/order/duplicates and only permits Template overrides
that narrow authority. Adapters translate a normalized copy into native settings;
Core and Docker never select native profile names. Every hosted execution peer
must support `local_environment_network_policy` and receive the complete bound
policy; missing policy never falls back to enabled or an older peer path. Read-only
workspace access does not require execution network policy. A declaration alone
does not qualify an image or admit public hosted creation.

Codex restricted networking uses its native managed network requirements and proxy.
Its adapter preserves the image's filesystem, approval and hook requirements and
adds the frozen exact-host ceiling in Session-private state. The existing RPC
client owns a bubblewrap child that mounts those requirements read-only and runs
the stock native app-server in a PID namespace; teardown retains the same owner.
Preparation regenerates the non-secret requirements from the frozen policy. Keep
the file with Session state so cleanup cannot race a running child's mount.
Claude and MiniMax translate the same policy into their native sandbox allowlists.
These translations do not add a Core network service or model/tool loop.

A dedicated local Runtime uses one Environment-scoped device credential and an
immutable binding to that Environment's Session. It is excluded from general
Expand All @@ -435,8 +463,8 @@ establish Provider lifecycle, or define the official `self_hosted` mapping.
Core rechecks the persisted Environment/device binding for preparation and active
reads; capability discovery cannot select or authorize a general device for this
placement. Local work uses the existing pending-input reservation and Worker
ownership without a remote connection resolver. The basic hosted profile supports `network.access: enabled` or `disabled`; the
actual image must qualify both native profiles before public deployment. Omitted
ownership without a remote connection resolver. The hosted profile supports `network.access: enabled`, `disabled` and exact-host
`restricted`; each image must qualify the supported policies before public deployment. Omitted
network settings mean enabled upstream and must not be silently treated as disabled.

Core and Runtime use common preparation, start, input-receipt, cancellation,
Expand Down
8 changes: 5 additions & 3 deletions apps/parsar-daemon/internal/agent/claudesdk/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,22 @@ import (
"os"
"path/filepath"
"strings"

"github.com/MiniMax-AI-Dev/parsar/internal/agentnetwork"
)

// ConfigureLocal selects the qualified, dedicated Runtime layout. The shared
// localworkspace binding still authorizes every request against its Session.
func ConfigureLocal(config Config, root, workspace, network, staging string) (Config, error) {
func ConfigureLocal(config Config, root, workspace string, network agentnetwork.Policy, staging string) (Config, error) {
config.StateDir = filepath.Join(root, "runtime", "claude-sdk", "history")
config.Workspace = &WorkspaceConfig{
Directory: workspace, PublicDirectory: "/workspace", NetworkAccess: network,
Directory: workspace, PublicDirectory: "/workspace", NetworkAccess: network.Access, AllowedDomains: network.Hosts(),
HomeDir: filepath.Join(root, "runtime", "claude-sdk", "home"),
ScratchDir: filepath.Join(root, "runtime", "claude-sdk", "scratch"),
ProtectedDirs: []string{filepath.Join(root, "parsar-daemon"), staging},
DependencyPath: "/usr/local/bin:/usr/bin:/bin",
}
if network != "enabled" && network != "disabled" {
if network.Validate() != nil {
return Config{}, fmt.Errorf("claudesdk: dedicated Runtime requires an explicit network policy")
}
for _, dir := range []string{config.StateDir, config.Workspace.HomeDir, config.Workspace.ScratchDir} {
Expand Down
18 changes: 18 additions & 0 deletions apps/parsar-daemon/internal/agent/claudesdk/local_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,24 @@ func TestLocalWorkspaceBindingNetworkAndRequiredHistory(t *testing.T) {
}
}

func TestRestrictedWorkspacePolicyUsesExactBoundAuthority(t *testing.T) {
config := workspaceFixture(t)
config.Workspace.NetworkAccess = "restricted"
config.Workspace.AllowedDomains = []string{"Example.com", "api.example.com", "example.com"}
req := workspaceRequest()
req.LocalEnvironment = &proto.LocalEnvironment{ID: "environment", NetworkAccess: "restricted", AllowedDomains: []string{"api.example.com", "EXAMPLE.COM"}}
start, _, err := prepare(config, req)
if err != nil || !slices.Equal(start.Workspace.AllowedDomains, []string{"api.example.com", "example.com"}) {
t.Fatal("native policy lost exact bound domains", start, err)
}
for _, domains := range [][]string{{"example.com"}, {"example.org"}, nil} {
req.LocalEnvironment.AllowedDomains = domains
if _, _, err := prepare(config, req); err == nil {
t.Fatal("different policy entered bound Runtime", domains)
}
}
}

func TestWorkspaceProviderCredentialsReplaceAmbientSelection(t *testing.T) {
config := workspaceFixture(t)
original := slices.Clone(config.Env)
Expand Down
9 changes: 6 additions & 3 deletions apps/parsar-daemon/internal/agent/claudesdk/workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/localworkspace"
"github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/paths"
"github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto"
"github.com/MiniMax-AI-Dev/parsar/internal/agentnetwork"
"github.com/MiniMax-AI-Dev/parsar/internal/agentskill"
)

Expand All @@ -21,6 +22,7 @@ type WorkspaceConfig struct {
Directory string
PublicDirectory string
NetworkAccess string
AllowedDomains []string
HomeDir string
ScratchDir string
ProtectedDirs []string
Expand All @@ -38,6 +40,7 @@ type workspaceProfile struct {
DependencyPath string `json:"dependency_path"`
EnvNames []string `json:"env_names"`
NetworkAccess string `json:"network_access,omitempty"`
AllowedDomains []string `json:"allowed_domains,omitempty"`
}

func prepareWorkspace(config Config, req proto.PromptRequestPayload) (*workspaceProfile, []string, error) {
Expand All @@ -47,7 +50,7 @@ func prepareWorkspace(config Config, req proto.PromptRequestPayload) (*workspace
if req.WorkDir != "" && req.WorkDir != config.Workspace.Directory {
return nil, nil, fmt.Errorf("claudesdk: work_dir conflicts with the trusted workspace binding")
}
if req.LocalEnvironment != nil && (config.Workspace.NetworkAccess == "" || req.LocalEnvironment.NetworkAccess != config.Workspace.NetworkAccess) {
if req.LocalEnvironment != nil && !(agentnetwork.Policy{Access: config.Workspace.NetworkAccess, AllowedDomains: config.Workspace.AllowedDomains}).Equal(agentnetwork.Policy{Access: req.LocalEnvironment.NetworkAccess, AllowedDomains: req.LocalEnvironment.AllowedDomains}) {
return nil, nil, fmt.Errorf("claudesdk: local Runtime network policy mismatch")
}
profile, env, err := workspaceEnvironment(config)
Expand Down Expand Up @@ -87,7 +90,7 @@ func workspaceEnvironment(config Config) (*workspaceProfile, []string, error) {
if w == nil || !filepath.IsAbs(config.Node) || !filepath.IsAbs(config.Entrypoint) {
return fail()
}
if w.NetworkAccess != "" && w.NetworkAccess != "disabled" && w.NetworkAccess != "enabled" {
if (w.NetworkAccess != "" || len(w.AllowedDomains) > 0) && (agentnetwork.Policy{Access: w.NetworkAccess, AllowedDomains: w.AllowedDomains}).Validate() != nil {
return fail()
}
if w.PublicDirectory != "" {
Expand Down Expand Up @@ -157,7 +160,7 @@ func workspaceEnvironment(config Config) (*workspaceProfile, []string, error) {
}
dependencyPath := strings.Join(dependencies, string(os.PathListSeparator))
profile := &workspaceProfile{Home: w.HomeDir, State: config.StateDir, Scratch: w.ScratchDir,
ProtectedDirs: append([]string{}, w.ProtectedDirs...), DependencyPath: dependencyPath, EnvNames: []string{}, NetworkAccess: w.NetworkAccess}
ProtectedDirs: append([]string{}, w.ProtectedDirs...), DependencyPath: dependencyPath, EnvNames: []string{}, NetworkAccess: w.NetworkAccess, AllowedDomains: (agentnetwork.Policy{Access: w.NetworkAccess, AllowedDomains: w.AllowedDomains}).Hosts()}
env := []string{"PATH=" + dependencyPath, "HOME=" + w.HomeDir, "TMPDIR=" + w.ScratchDir,
"CLAUDE_CONFIG_DIR=" + config.StateDir, "DISABLE_TELEMETRY=1", "DISABLE_ERROR_REPORTING=1",
"DISABLE_AUTOUPDATER=1", "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1", "CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1"}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@ func SupportsLocalNetworkPolicy(version string) bool {
return false
}
binding, err := localworkspace.Load()
return err == nil && binding != nil && binding.NetworkAccess() != ""
return err == nil && binding != nil && binding.NetworkPolicy().Validate() == nil
}
113 changes: 113 additions & 0 deletions apps/parsar-daemon/internal/agent/codex/managed_network.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package codex

import (
"bytes"
"errors"
"os"
"os/exec"
"path/filepath"
"strings"

"github.com/BurntSushi/toml"
"github.com/MiniMax-AI-Dev/parsar/internal/agentnetwork"
)

const nativeManagedRequirements = "/etc/codex/requirements.toml"

// Preserve the image's native filesystem, approval and hook requirements. This
// adapter adds the exact network ceiling; Core never supplies native TOML.
func managedNetworkRequirements(base []byte, policy agentnetwork.Policy) ([]byte, error) {
if policy.Access != "restricted" || policy.Validate() != nil {
return nil, errors.New("codex: invalid restricted network policy")
}
var config map[string]any
if _, err := toml.Decode(string(base), &config); err != nil {
return nil, err
}
if _, exists := config["experimental_network"]; exists {
return nil, errors.New("codex: image already defines managed network requirements")
}
domains := make(map[string]string)
for _, host := range policy.Hosts() {
domains[host] = "allow"
}
var addition bytes.Buffer
err := toml.NewEncoder(&addition).Encode(map[string]any{"experimental_network": map[string]any{
"enabled": true, "managed_allowed_domains_only": true,
"allow_upstream_proxy": false, "dangerously_allow_all_unix_sockets": false,
"allow_local_binding": false, "domains": domains,
}})
if err != nil {
return nil, err
}
result := append(append([]byte{}, base...), '\n')
return append(result, addition.Bytes()...), nil
}

func prepareManagedNetwork(plan *SessionPlan, policy agentnetwork.Policy) error {
var home string
for _, entry := range plan.Env {
if value, found := strings.CutPrefix(entry, "CODEX_HOME="); found {
home = value
}
}
if !filepath.IsAbs(home) {
return errors.New("codex: managed network requires private Session state")
}
base, err := os.ReadFile(nativeManagedRequirements)
if err != nil {
return err
}
contents, err := managedNetworkRequirements(base, policy)
if err != nil {
return err
}
file, err := os.CreateTemp(home, ".managed-network-*.toml")
if err != nil {
return err
}
defer os.Remove(file.Name())
if _, err = file.Write(contents); err == nil {
err = file.Chmod(0400)
}
closeErr := file.Close()
if err != nil {
return err
}
if closeErr != nil {
return closeErr
}
path := filepath.Join(home, "managed-network.toml")
if err = os.Rename(file.Name(), path); err != nil {
return err
}
// Retain this non-secret file with Session state. Rebuild it from the frozen
// policy on preparation; cleanup never races a process still holding its mount.
plan.managedRequirements = path
plan.ExtraConfig = append(plan.ExtraConfig, [2]string{"features.network_proxy", "true"})
return nil
}

// The existing RPC client owns the sole child, its pipes and teardown. The PID
// namespace terminates native descendants when that owned process is killed.
func configureManagedNetworkProcess(cfg *JSONRPCConfig, requirements string) error {
if requirements == "" {
return nil
}
if !filepath.IsAbs(requirements) {
return errors.New("codex: managed requirements path must be absolute")
}
binary, err := exec.LookPath(cfg.Binary)
if err != nil {
return err
}
binary, err = filepath.Abs(binary)
if err != nil {
return err
}
cfg.Binary = "/usr/bin/bwrap"
cfg.ExtraArgs = append([]string{"--die-with-parent", "--unshare-pid", "--bind", "/", "/",
"--dev-bind", "/dev", "/dev", "--proc", "/proc", "--ro-bind", requirements,
nativeManagedRequirements, binary}, cfg.ExtraArgs...)
return nil
}
Loading
Loading