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
16 changes: 14 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,14 +288,26 @@ Completed environments never reinstall initial files on reconnect or native reco
Provider RunCommand carries bounded stdin, not confidential argv. Only fixed trusted
initializers may run with Runtime authority. User setup and package install hooks
run in the common packaged sandbox, without daemon credentials or native history.
Files and npm/Python packages precede ordered setup commands. Initialization has
Files, inline Skills and npm/Python packages precede ordered setup commands. Initialization has
provisioning network access; requested network restrictions apply to native tools
after setup. Confidential env and setup snapshots are encrypted independently of
ordinary metadata. Adapters apply tool env only after isolation, never to the
credential-bearing daemon/native harness launcher.
Reuse the packaged atomic file writer and anchored parent creation across all profiles.

Name, enabled/disabled network, initial files and env/setup/npm/Python are
Inline Skill ZIPs use the same confidential initialization snapshot and installer.
Core validates portable manifests and bounded regular-file archives, returns only
safe Skill metadata, and freezes content before native preparation. The Runtime
owns `/environment/initialization/capabilities/skills/<name>`; setup and native tools may read but
not modify this tree. The common execution descriptor carries Skill metadata,
never native plugin configuration or template identities. Adapters register native
Skill roots without changing the execution loop or enabling unrestricted tools.
Native activation extensions remain adapter-owned and must fail explicitly when
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/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
Expand Down
24 changes: 16 additions & 8 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/agentskill"
)

// WorkspaceConfig binds one trusted private placement. It does not create an
Expand All @@ -27,14 +28,15 @@ type WorkspaceConfig struct {
}

type workspaceProfile struct {
ToolEnvironment bool `json:"tool_environment,omitempty"`
Home string `json:"home"`
State string `json:"state"`
Scratch string `json:"scratch"`
ProtectedDirs []string `json:"protected_dirs"`
DependencyPath string `json:"dependency_path"`
EnvNames []string `json:"env_names"`
NetworkAccess string `json:"network_access,omitempty"`
Skills []agentskill.Metadata `json:"skills,omitempty"`
ToolEnvironment bool `json:"tool_environment,omitempty"`
Home string `json:"home"`
State string `json:"state"`
Scratch string `json:"scratch"`
ProtectedDirs []string `json:"protected_dirs"`
DependencyPath string `json:"dependency_path"`
EnvNames []string `json:"env_names"`
NetworkAccess string `json:"network_access,omitempty"`
}

func prepareWorkspace(config Config, req proto.PromptRequestPayload) (*workspaceProfile, []string, error) {
Expand All @@ -57,6 +59,12 @@ func prepareWorkspace(config Config, req proto.PromptRequestPayload) (*workspace
}
profile.ToolEnvironment = true
}
if req.LocalEnvironment != nil {
if err := localworkspace.VerifySkills(req.LocalEnvironment.Skills); err != nil {
return nil, nil, err
}
profile.Skills = req.LocalEnvironment.Skills
}
return profile, env, nil
}

Expand Down
44 changes: 44 additions & 0 deletions apps/parsar-daemon/internal/agent/codex/hosted_skills.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package codex

import (
"fmt"
"io/fs"
"os"
"path/filepath"

"github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/localworkspace"
"github.com/MiniMax-AI-Dev/parsar/internal/agentskill"
)

func verifyHostedSkills(skills []agentskill.Metadata) error {
if err := localworkspace.VerifySkills(skills); err != nil {
return err
}
for _, skill := range skills {
if err := verifyHostedSkillLayout(filepath.Join(localworkspace.SkillDirectory, skill.Name)); err != nil {
return err
}
}
return nil
}

func verifyHostedSkillLayout(root string) error {
// Native dependency declarations may start MCP installation outside the
// workspace tool sandbox. They are not qualified by an inert Skill upload.
if _, err := os.Lstat(filepath.Join(root, "agents", "openai.yaml")); err == nil {
return fmt.Errorf("codex: native Skill configuration is unsupported")
} else if !os.IsNotExist(err) {
return err
}
// Extra roots are scanned recursively. One public Skill must not silently
// introduce additional native Skills with their own activation metadata.
return filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error {
if err != nil {
return err
}
if entry.Name() == "SKILL.md" && path != filepath.Join(root, "SKILL.md") {
return fmt.Errorf("codex: nested native Skills are unsupported")
}
return nil
})
}
36 changes: 36 additions & 0 deletions apps/parsar-daemon/internal/agent/codex/hosted_skills_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package codex

import (
"os"
"path/filepath"
"testing"
)

func TestHostedSkillDoesNotActivateAdditionalNativeResources(t *testing.T) {
for _, tc := range []struct {
name string
files []string
rejected bool
}{
{"ordinary supporting files", []string{"SKILL.md", "scripts/check.py", "references/guide.md"}, false},
{"native dependency configuration", []string{"SKILL.md", "agents/openai.yaml"}, true},
{"nested discovery", []string{"SKILL.md", "references/other/SKILL.md"}, true},
{"nested dependencies", []string{"SKILL.md", "references/other/SKILL.md", "references/other/agents/openai.yaml"}, true},
} {
t.Run(tc.name, func(t *testing.T) {
root := t.TempDir()
for _, name := range tc.files {
path := filepath.Join(root, name)
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte("fixture"), 0400); err != nil {
t.Fatal(err)
}
}
if err := verifyHostedSkillLayout(root); (err != nil) != tc.rejected {
t.Fatalf("rejected=%v err=%v", tc.rejected, err)
}
})
}
}
7 changes: 6 additions & 1 deletion apps/parsar-daemon/internal/agent/codex/session_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,12 @@ func prepareSessionPlan(ctx context.Context, req proto.PromptRequestPayload, cfg
plan.Env = append(plan.Env, "CODEX_EXEC_SERVER_URL=none")
}
skillRoot := ""
if !req.DisableExecutionEnvironment && req.RemoteEnvironment == nil {
if req.LocalEnvironment != nil && len(req.LocalEnvironment.Skills) > 0 {
err = verifyHostedSkills(req.LocalEnvironment.Skills)
if err == nil {
skillRoot = localworkspace.SkillDirectory
}
} else if !req.DisableExecutionEnvironment && req.RemoteEnvironment == nil {
skillRoot, err = prepareManagedSkills(ctx, cfg.logger, req)
}
if err != nil {
Expand Down
18 changes: 12 additions & 6 deletions apps/parsar-daemon/internal/agent/mcode/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ type launchOptions struct {
}

func prepareOptions(ctx context.Context, req proto.PromptRequestPayload) (launchOptions, error) {
return prepareOptionsWithSkills(ctx, req, true)
}

func prepareOptionsWithSkills(ctx context.Context, req proto.PromptRequestPayload, managedSkills bool) (launchOptions, error) {
var result launchOptions
if req.StrictResume {
if err := validateExecutionRequest(req); err != nil {
Expand All @@ -48,12 +52,14 @@ func prepareOptions(ctx context.Context, req proto.PromptRequestPayload) (launch
return result, err
}
}
installed, err := claudecode.InstallManagedSkills(ctx, log.With("component", "mcode"), root, req.AgentOptions["skills"])
if err != nil {
return result, err
}
if len(installed.Warnings) > 0 {
return result, fmt.Errorf("mcode: one or more configured Skills could not be installed")
if managedSkills {
installed, err := claudecode.InstallManagedSkills(ctx, log.With("component", "mcode"), root, req.AgentOptions["skills"])
if err != nil {
return result, err
}
if len(installed.Warnings) > 0 {
return result, fmt.Errorf("mcode: one or more configured Skills could not be installed")
}
}
opts := req.AgentOptions
prompt := optionString(opts, "system_prompt")
Expand Down
19 changes: 17 additions & 2 deletions apps/parsar-daemon/internal/agent/mcode/workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,25 @@ func prepareWorkspaceOptions(ctx context.Context, c WorkspaceConfig, req proto.P
// cwd remains private; only the internal MCP worker receives the public workspace.
private := req
private.LocalEnvironment, private.WorkDir, private.DisableExecutionEnvironment = nil, "", true
opts, err := prepareOptions(ctx, private)
opts, err := prepareOptionsWithSkills(ctx, private, false)
if err != nil {
return opts, err
}
if err := localworkspace.VerifySkills(req.LocalEnvironment.Skills); err != nil {
return opts, err
}
if len(req.LocalEnvironment.Skills) > 0 {
link := filepath.Join(opts.DataDir, "skills")
if target, err := os.Readlink(link); err == nil {
if target != localworkspace.SkillDirectory {
return opts, fmt.Errorf("mcode: unexpected native Skill root")
}
} else if !os.IsNotExist(err) {
return opts, err
} else if err := os.Symlink(localworkspace.SkillDirectory, link); err != nil {
return opts, err
}
}
raw, err := os.ReadFile(filepath.Join(opts.DataDir, "config.yaml"))
if err != nil {
return opts, err
Expand All @@ -77,7 +92,7 @@ func prepareWorkspaceOptions(ctx context.Context, c WorkspaceConfig, req proto.P
if err = os.WriteFile(filepath.Join(opts.DataDir, "config.yaml"), raw, 0600); err != nil {
return opts, err
}
profile := map[string]any{"workspace": "/workspace", "scratch": c.Scratch, "network": c.Network, "protectedDirs": slices.Clone(c.ProtectedDirs)}
profile := map[string]any{"workspace": "/workspace", "scratch": c.Scratch, "network": c.Network, "protectedDirs": slices.Clone(c.ProtectedDirs), "skills": len(req.LocalEnvironment.Skills) > 0}
if req.LocalEnvironment.ToolEnvironment {
if err := localworkspace.VerifyToolEnvironment(); err != nil {
return opts, err
Expand Down
60 changes: 60 additions & 0 deletions apps/parsar-daemon/internal/localworkspace/skills.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package localworkspace

import (
"errors"
"io/fs"
"os"
"path/filepath"

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

const CapabilityDirectory = "/environment/initialization/capabilities"
const SkillDirectory = CapabilityDirectory + "/skills"

// VerifySkills consumes the common initialized layout, independently of native loading.
func VerifySkills(skills []agentskill.Metadata) error {
if len(skills) == 0 {
return nil
}
for _, directory := range []string{CapabilityDirectory, SkillDirectory} {
actual, err := filepath.EvalSymlinks(directory)
if err != nil || actual != directory {
return errors.New("initialized Skill directory unavailable")
}
}
seen := map[string]bool{}
for _, metadata := range skills {
if metadata.Type != "inline" || metadata.Name == "" || filepath.Base(metadata.Name) != metadata.Name || metadata.Name == "." || metadata.Name == ".." || seen[metadata.Name] {
return agentskill.ErrInvalid
}
seen[metadata.Name] = true
root := filepath.Join(SkillDirectory, metadata.Name)
count, total := 0, int64(0)
if err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error {
if err != nil {
return agentskill.ErrInvalid
}
if entry.IsDir() {
return nil
}
info, err := entry.Info()
if err != nil || !info.Mode().IsRegular() || info.Mode().Perm()&0222 != 0 {
return agentskill.ErrInvalid
}
count++
total += info.Size()
if count > agentskill.MaxFiles || total > agentskill.MaxExpandedBytes {
return agentskill.ErrInvalid
}
return nil
}); err != nil {
return err
}
body, err := os.ReadFile(filepath.Join(root, "SKILL.md"))
if err != nil || agentskill.ValidateManifest(body, metadata) != nil {
return agentskill.ErrInvalid
}
}
return nil
}
4 changes: 2 additions & 2 deletions contracts/agents-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ the Python SDK. Vault HTTP paths start at `/vaults`, not `/agents/vaults`.
| sessions.subagents.turns.items | list | Missing |
| environments | retrieve | Supported Codex self-hosted and three-harness Docker/E2B hosted profiles: durable status and safe initial-file metadata; other installation inventory and full lifecycle parity remain gaps |
| environments.files | create, list | [Bounded live listing and inline/source-file creation](environment-files.md) on qualified Docker/E2B workspaces; Codex self-hosted listing is a separate supported path. Full listing, overwrite and error semantics remain partial |
| environments.templates | create, retrieve, update, list, delete | [Reusable network/initial-file configuration and Session snapshots](environment-templates.md); other initialization and full semantics remain gaps |
| environments.templates | create, retrieve, update, list, delete | [Reusable network, files, env/setup/packages, inline Skills and Session snapshots](environment-templates.md); other initialization and full semantics remain gaps |
| vaults | create, retrieve, list, delete | Create/retrieve/list/delete with independent tenant persistence, stored status filtering, atomic Credential cascade and frozen Session attachments; archive semantics and full hosted lifecycle parity remain missing |
| vaults.credentials | create, retrieve, update, list, delete | Static-bearer create/retrieve/list/token replacement/deletion with scoped encrypted storage; Session attachment and exact-URL HTTPS MCP binding; OAuth, archive semantics and full hosted lifecycle parity remain missing |

Expand Down Expand Up @@ -155,7 +155,7 @@ user-managed enrollment remain outside this qualification.
| Area | Missing or unverified scope |
| --- | --- |
| Subagents / multi_agent | Six public child read operations, enabled execution, child lifecycle/interactions and full recovery; deferred outside the MVP |
| Environment Templates | Other populated initialization, restricted network, referenced files overrides/null network and exact hosted errors; CRUD/list, initial files and Session references are supported |
| Environment Templates | Skills references, Plugins, system packages, capability directories, restricted network, installation overrides/null network and exact hosted errors; CRUD/list, files, env/setup/npm/Python, inline Skills and Session references are supported |
| Input and configuration | Non-text initial input, broader content/configuration unions, structured output and reasoning/verbosity combinations |
| Tools and interactions | Deferred functions, other tool types, effective tool-set enforcement and result/cancel publication ordering; MiniMax public functions/MCP remain unsupported |
| Vault and Credentials | OAuth/refresh, archive semantics, revocation/concurrent mutation and exact hosted selection/error behavior; static bearer CRUD/token replacement is already present |
Expand Down
Loading
Loading