* Indicates that Windows does not support expos_paths, adding more mount paths for the Linux platform.
This commit is contained in:
parent
96f385d433
commit
4626989407
8 changed files with 304 additions and 24 deletions
|
|
@ -54,7 +54,7 @@ type IsolationConfig struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExposePath describes a host path that should remain visible inside the isolated
|
// ExposePath describes a host path that should remain visible inside the isolated
|
||||||
// child-process environment.
|
// child-process environment. This is currently implemented on Linux only.
|
||||||
type ExposePath struct {
|
type ExposePath struct {
|
||||||
Source string `json:"source"`
|
Source string `json:"source"`
|
||||||
Target string `json:"target,omitempty"`
|
Target string `json:"target,omitempty"`
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ Isolation lives under:
|
||||||
Field meanings:
|
Field meanings:
|
||||||
|
|
||||||
- `enabled`: enables or disables subprocess isolation. Default: `false`.
|
- `enabled`: enables or disables subprocess isolation. Default: `false`.
|
||||||
- `expose_paths`: explicitly exposes host paths inside the isolated environment. It only matters when `enabled=true`.
|
- `expose_paths`: explicitly exposes host paths inside the isolated environment. It only matters when `enabled=true`. This is currently supported on Linux only.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
|
|
@ -82,7 +82,7 @@ Rules for `expose_paths`:
|
||||||
Platform note:
|
Platform note:
|
||||||
|
|
||||||
- Linux uses a real `source -> target` mount view.
|
- Linux uses a real `source -> target` mount view.
|
||||||
- Windows currently keeps `target` for config shape, but access control is still based on `source`.
|
- Windows does not currently support `expose_paths`.
|
||||||
|
|
||||||
## Instance Root And Directories
|
## Instance Root And Directories
|
||||||
|
|
||||||
|
|
@ -173,6 +173,10 @@ Disabling isolation increases the risk that child processes can access or modify
|
||||||
|
|
||||||
### Windows
|
### Windows
|
||||||
|
|
||||||
|
Windows isolation currently supports process-level restrictions such as restricted tokens, low integrity, job objects, and redirected user-environment directories.
|
||||||
|
|
||||||
|
`expose_paths` is not currently supported on Windows. If it is configured, startup should fail instead of pretending the paths were exposed.
|
||||||
|
|
||||||
The Windows backend currently uses:
|
The Windows backend currently uses:
|
||||||
|
|
||||||
- a restricted primary token
|
- a restricted primary token
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@
|
||||||
字段说明:
|
字段说明:
|
||||||
|
|
||||||
- `enabled`:是否启用子进程隔离。默认值:`false`。
|
- `enabled`:是否启用子进程隔离。默认值:`false`。
|
||||||
- `expose_paths`:显式把宿主路径带入隔离环境。仅在 `enabled=true` 时生效。
|
- `expose_paths`:显式把宿主路径带入隔离环境。仅在 `enabled=true` 时生效。目前只在 Linux 上支持。
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
|
|
||||||
|
|
@ -82,7 +82,7 @@
|
||||||
平台说明:
|
平台说明:
|
||||||
|
|
||||||
- Linux 会真实使用 `source -> target` 挂载视图。
|
- Linux 会真实使用 `source -> target` 挂载视图。
|
||||||
- Windows 当前保留 `target` 结构,但权限控制仍以 `source` 为准。
|
- Windows 当前不支持 `expose_paths`。
|
||||||
|
|
||||||
## 实例根与目录
|
## 实例根与目录
|
||||||
|
|
||||||
|
|
@ -173,6 +173,10 @@ Linux 后端当前依赖 `bwrap`(`bubblewrap`)。
|
||||||
|
|
||||||
### Windows
|
### Windows
|
||||||
|
|
||||||
|
Windows 隔离当前提供的是进程级限制,例如 restricted token、low integrity、job object,以及用户环境目录重定向。
|
||||||
|
|
||||||
|
`expose_paths` 目前不支持 Windows。如果配置了该字段,启动应直接失败,而不是假装这些路径已经被暴露进隔离环境。
|
||||||
|
|
||||||
Windows 后端当前使用:
|
Windows 后端当前使用:
|
||||||
|
|
||||||
- 受限 primary token
|
- 受限 primary token
|
||||||
|
|
|
||||||
|
|
@ -39,11 +39,12 @@ func applyPlatformIsolation(cmd *exec.Cmd, isolation config.IsolationConfig, roo
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{}
|
cmd.SysProcAttr = &syscall.SysProcAttr{}
|
||||||
}
|
}
|
||||||
rules := BuildWindowsAccessRules(root, isolation.ExposePaths)
|
rules := BuildWindowsAccessRules(root, isolation.ExposePaths)
|
||||||
logger.InfoCF("isolation", "windows isolation access rules",
|
logger.InfoCF("isolation", "windows isolation process constraints",
|
||||||
map[string]any{
|
map[string]any{
|
||||||
"root": root,
|
"root": root,
|
||||||
"command": cmd.Path,
|
"command": cmd.Path,
|
||||||
"rules": formatWindowsAccessRules(rules),
|
"rules": formatWindowsAccessRules(rules),
|
||||||
|
"note": "Windows currently enforces restricted token, low integrity, and job object limits; expose_paths filesystem remapping is rejected during preflight",
|
||||||
})
|
})
|
||||||
// Create the restricted token before the process starts so CreateProcess uses
|
// Create the restricted token before the process starts so CreateProcess uses
|
||||||
// the reduced privilege set from the first instruction.
|
// the reduced privilege set from the first instruction.
|
||||||
|
|
|
||||||
|
|
@ -227,17 +227,33 @@ func DefaultExposePaths(root string) []config.ExposePath {
|
||||||
Mode: "rw",
|
Mode: "rw",
|
||||||
}}
|
}}
|
||||||
if runtime.GOOS == "linux" {
|
if runtime.GOOS == "linux" {
|
||||||
items = append(items,
|
items = append(items, defaultLinuxSystemExposePaths()...)
|
||||||
config.ExposePath{Source: "/usr", Target: "/usr", Mode: "ro"},
|
|
||||||
config.ExposePath{Source: "/bin", Target: "/bin", Mode: "ro"},
|
|
||||||
config.ExposePath{Source: "/lib", Target: "/lib", Mode: "ro"},
|
|
||||||
config.ExposePath{Source: "/lib64", Target: "/lib64", Mode: "ro"},
|
|
||||||
config.ExposePath{Source: "/etc/resolv.conf", Target: "/etc/resolv.conf", Mode: "ro"},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func defaultLinuxSystemExposePaths() []config.ExposePath {
|
||||||
|
return []config.ExposePath{
|
||||||
|
{Source: "/usr", Target: "/usr", Mode: "ro"},
|
||||||
|
{Source: "/bin", Target: "/bin", Mode: "ro"},
|
||||||
|
{Source: "/lib", Target: "/lib", Mode: "ro"},
|
||||||
|
{Source: "/lib64", Target: "/lib64", Mode: "ro"},
|
||||||
|
{Source: "/etc/resolv.conf", Target: "/etc/resolv.conf", Mode: "ro"},
|
||||||
|
{Source: "/etc/hosts", Target: "/etc/hosts", Mode: "ro"},
|
||||||
|
{Source: "/etc/nsswitch.conf", Target: "/etc/nsswitch.conf", Mode: "ro"},
|
||||||
|
{Source: "/etc/passwd", Target: "/etc/passwd", Mode: "ro"},
|
||||||
|
{Source: "/etc/group", Target: "/etc/group", Mode: "ro"},
|
||||||
|
{Source: "/etc/ssl", Target: "/etc/ssl", Mode: "ro"},
|
||||||
|
{Source: "/etc/pki", Target: "/etc/pki", Mode: "ro"},
|
||||||
|
{Source: "/etc/ca-certificates", Target: "/etc/ca-certificates", Mode: "ro"},
|
||||||
|
{Source: "/usr/share/ca-certificates", Target: "/usr/share/ca-certificates", Mode: "ro"},
|
||||||
|
{Source: "/usr/local/share/ca-certificates", Target: "/usr/local/share/ca-certificates", Mode: "ro"},
|
||||||
|
{Source: "/etc/alternatives", Target: "/etc/alternatives", Mode: "ro"},
|
||||||
|
{Source: "/usr/share/zoneinfo", Target: "/usr/share/zoneinfo", Mode: "ro"},
|
||||||
|
{Source: "/etc/localtime", Target: "/etc/localtime", Mode: "ro"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MergeExposePaths merges built-in rules with user overrides. Rules are keyed
|
// MergeExposePaths merges built-in rules with user overrides. Rules are keyed
|
||||||
// by target path so later entries replace earlier ones for the same target.
|
// by target path so later entries replace earlier ones for the same target.
|
||||||
func MergeExposePaths(defaults []config.ExposePath, overrides []config.ExposePath) []config.ExposePath {
|
func MergeExposePaths(defaults []config.ExposePath, overrides []config.ExposePath) []config.ExposePath {
|
||||||
|
|
@ -276,21 +292,19 @@ func BuildLinuxMountPlan(root string, overrides []config.ExposePath) []MountRule
|
||||||
// Windows restricted-token backend.
|
// Windows restricted-token backend.
|
||||||
func BuildWindowsAccessRules(root string, overrides []config.ExposePath) []AccessRule {
|
func BuildWindowsAccessRules(root string, overrides []config.ExposePath) []AccessRule {
|
||||||
rules := []AccessRule{{Path: root, Mode: "rw"}}
|
rules := []AccessRule{{Path: root, Mode: "rw"}}
|
||||||
if userProfile := os.Getenv("USERPROFILE"); userProfile != "" {
|
|
||||||
rules = append(rules,
|
|
||||||
AccessRule{Path: filepath.Join(userProfile, ".ssh"), Mode: "deny"},
|
|
||||||
AccessRule{Path: filepath.Join(userProfile, ".gitconfig"), Mode: "deny"},
|
|
||||||
AccessRule{Path: filepath.Join(userProfile, "Documents"), Mode: "deny"},
|
|
||||||
AccessRule{Path: filepath.Join(userProfile, "Desktop"), Mode: "deny"},
|
|
||||||
AccessRule{Path: filepath.Join(userProfile, "Downloads"), Mode: "deny"},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
for _, item := range MergeExposePaths(nil, overrides) {
|
for _, item := range MergeExposePaths(nil, overrides) {
|
||||||
rules = append(rules, AccessRule{Path: item.Source, Mode: item.Mode})
|
rules = append(rules, AccessRule{Path: item.Source, Mode: item.Mode})
|
||||||
}
|
}
|
||||||
return rules
|
return rules
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateWindowsExposePaths(items []config.ExposePath) error {
|
||||||
|
if len(items) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("windows isolation does not yet support expose_paths filesystem rules")
|
||||||
|
}
|
||||||
|
|
||||||
// IsSupported reports whether the current platform has an implemented isolation
|
// IsSupported reports whether the current platform has an implemented isolation
|
||||||
// backend.
|
// backend.
|
||||||
func IsSupported() bool {
|
func IsSupported() bool {
|
||||||
|
|
@ -334,6 +348,9 @@ func Preflight() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
|
if err := validateWindowsExposePaths(isolation.ExposePaths); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
for _, rule := range BuildWindowsAccessRules(root, isolation.ExposePaths) {
|
for _, rule := range BuildWindowsAccessRules(root, isolation.ExposePaths) {
|
||||||
if rule.Path == "" {
|
if rule.Path == "" {
|
||||||
return fmt.Errorf("invalid windows access rule")
|
return fmt.Errorf("invalid windows access rule")
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,6 @@ func TestBuildLinuxMountPlan(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildWindowsAccessRules(t *testing.T) {
|
func TestBuildWindowsAccessRules(t *testing.T) {
|
||||||
t.Setenv("USERPROFILE", `C:\Users\tester`)
|
|
||||||
rules := BuildWindowsAccessRules(
|
rules := BuildWindowsAccessRules(
|
||||||
`C:\picoclaw`,
|
`C:\picoclaw`,
|
||||||
[]config.ExposePath{{Source: `D:\data`, Target: `C:\mapped`, Mode: "ro"}},
|
[]config.ExposePath{{Source: `D:\data`, Target: `C:\mapped`, Mode: "ro"}},
|
||||||
|
|
@ -140,6 +139,37 @@ func TestBuildWindowsAccessRules(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateWindowsExposePaths(t *testing.T) {
|
||||||
|
if err := validateWindowsExposePaths(nil); err != nil {
|
||||||
|
t.Fatalf("validateWindowsExposePaths(nil) error = %v", err)
|
||||||
|
}
|
||||||
|
err := validateWindowsExposePaths([]config.ExposePath{{Source: `D:\data`, Target: `D:\data`, Mode: "ro"}})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("validateWindowsExposePaths() expected error for expose_paths")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaultLinuxSystemExposePaths(t *testing.T) {
|
||||||
|
paths := defaultLinuxSystemExposePaths()
|
||||||
|
needed := map[string]bool{
|
||||||
|
"/etc/hosts": false,
|
||||||
|
"/etc/nsswitch.conf": false,
|
||||||
|
"/etc/ssl": false,
|
||||||
|
"/usr/share/zoneinfo": false,
|
||||||
|
"/etc/localtime": false,
|
||||||
|
}
|
||||||
|
for _, item := range paths {
|
||||||
|
if _, ok := needed[item.Source]; ok {
|
||||||
|
needed[item.Source] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for path, found := range needed {
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("defaultLinuxSystemExposePaths missing %s", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPrepareCommand_AppliesUserEnv(t *testing.T) {
|
func TestPrepareCommand_AppliesUserEnv(t *testing.T) {
|
||||||
t.Setenv(config.EnvHome, filepath.Join(t.TempDir(), "home"))
|
t.Setenv(config.EnvHome, filepath.Join(t.TempDir(), "home"))
|
||||||
if runtime.GOOS == "linux" {
|
if runtime.GOOS == "linux" {
|
||||||
|
|
|
||||||
224
pkg/mcp/isolated_command_transport.go
Normal file
224
pkg/mcp/isolated_command_transport.go
Normal file
|
|
@ -0,0 +1,224 @@
|
||||||
|
package mcp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os/exec"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
|
||||||
|
sdkmcp "github.com/modelcontextprotocol/go-sdk/mcp"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/isolation"
|
||||||
|
)
|
||||||
|
|
||||||
|
var isolatedCommandTerminateDuration = 5 * time.Second
|
||||||
|
|
||||||
|
// isolatedCommandTransport mirrors the SDK command transport but routes
|
||||||
|
// process startup through pkg/isolation so Windows post-start hooks run too.
|
||||||
|
type isolatedCommandTransport struct {
|
||||||
|
Command *exec.Cmd
|
||||||
|
TerminateDuration time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *isolatedCommandTransport) Connect(ctx context.Context) (sdkmcp.Connection, error) {
|
||||||
|
stdout, err := t.Command.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
stdout = io.NopCloser(stdout)
|
||||||
|
stdin, err := t.Command.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := isolation.Start(t.Command); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
td := t.TerminateDuration
|
||||||
|
if td <= 0 {
|
||||||
|
td = isolatedCommandTerminateDuration
|
||||||
|
}
|
||||||
|
return newIsolatedIOConn(&isolatedPipeRWC{cmd: t.Command, stdout: stdout, stdin: stdin, terminateDuration: td}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type isolatedPipeRWC struct {
|
||||||
|
cmd *exec.Cmd
|
||||||
|
stdout io.ReadCloser
|
||||||
|
stdin io.WriteCloser
|
||||||
|
terminateDuration time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *isolatedPipeRWC) Read(p []byte) (n int, err error) {
|
||||||
|
return s.stdout.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *isolatedPipeRWC) Write(p []byte) (n int, err error) {
|
||||||
|
return s.stdin.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *isolatedPipeRWC) Close() error {
|
||||||
|
if err := s.stdin.Close(); err != nil {
|
||||||
|
return fmt.Errorf("closing stdin: %v", err)
|
||||||
|
}
|
||||||
|
resChan := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
resChan <- s.cmd.Wait()
|
||||||
|
}()
|
||||||
|
wait := func() (error, bool) {
|
||||||
|
select {
|
||||||
|
case err := <-resChan:
|
||||||
|
return err, true
|
||||||
|
case <-time.After(s.terminateDuration):
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
if err, ok := wait(); ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.cmd.Process.Signal(syscall.SIGTERM); err == nil {
|
||||||
|
if err, ok := wait(); ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := s.cmd.Process.Kill(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err, ok := wait(); ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return fmt.Errorf("unresponsive subprocess")
|
||||||
|
}
|
||||||
|
|
||||||
|
type isolatedIOConn struct {
|
||||||
|
writeMu sync.Mutex
|
||||||
|
rwc io.ReadWriteCloser
|
||||||
|
incoming <-chan isolatedMsgOrErr
|
||||||
|
queue []jsonrpc.Message
|
||||||
|
closeOnce sync.Once
|
||||||
|
closed chan struct{}
|
||||||
|
closeErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
type isolatedMsgOrErr struct {
|
||||||
|
msg json.RawMessage
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func newIsolatedIOConn(rwc io.ReadWriteCloser) *isolatedIOConn {
|
||||||
|
incoming := make(chan isolatedMsgOrErr)
|
||||||
|
closed := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
dec := json.NewDecoder(rwc)
|
||||||
|
for {
|
||||||
|
var raw json.RawMessage
|
||||||
|
err := dec.Decode(&raw)
|
||||||
|
if err == nil {
|
||||||
|
var tr [1]byte
|
||||||
|
if n, readErr := dec.Buffered().Read(tr[:]); n > 0 {
|
||||||
|
if tr[0] != '\n' && tr[0] != '\r' {
|
||||||
|
err = fmt.Errorf("invalid trailing data at the end of stream")
|
||||||
|
}
|
||||||
|
} else if readErr != nil && readErr != io.EOF {
|
||||||
|
err = readErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case incoming <- isolatedMsgOrErr{msg: raw, err: err}:
|
||||||
|
case <-closed:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return &isolatedIOConn{rwc: rwc, incoming: incoming, closed: closed}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *isolatedIOConn) SessionID() string { return "" }
|
||||||
|
|
||||||
|
func (c *isolatedIOConn) Read(ctx context.Context) (jsonrpc.Message, error) {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
if len(c.queue) > 0 {
|
||||||
|
next := c.queue[0]
|
||||||
|
c.queue = c.queue[1:]
|
||||||
|
return next, nil
|
||||||
|
}
|
||||||
|
var raw json.RawMessage
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
case v := <-c.incoming:
|
||||||
|
if v.err != nil {
|
||||||
|
return nil, v.err
|
||||||
|
}
|
||||||
|
raw = v.msg
|
||||||
|
case <-c.closed:
|
||||||
|
return nil, io.EOF
|
||||||
|
}
|
||||||
|
msgs, err := readIsolatedBatch(raw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c.queue = msgs[1:]
|
||||||
|
return msgs[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readIsolatedBatch(data []byte) ([]jsonrpc.Message, error) {
|
||||||
|
var rawBatch []json.RawMessage
|
||||||
|
if err := json.Unmarshal(data, &rawBatch); err == nil {
|
||||||
|
if len(rawBatch) == 0 {
|
||||||
|
return nil, fmt.Errorf("empty batch")
|
||||||
|
}
|
||||||
|
msgs := make([]jsonrpc.Message, 0, len(rawBatch))
|
||||||
|
for _, raw := range rawBatch {
|
||||||
|
msg, err := jsonrpc.DecodeMessage(raw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
msgs = append(msgs, msg)
|
||||||
|
}
|
||||||
|
return msgs, nil
|
||||||
|
}
|
||||||
|
msg, err := jsonrpc.DecodeMessage(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return []jsonrpc.Message{msg}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *isolatedIOConn) Write(ctx context.Context, msg jsonrpc.Message) error {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
c.writeMu.Lock()
|
||||||
|
defer c.writeMu.Unlock()
|
||||||
|
data, err := jsonrpc.EncodeMessage(msg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshaling message: %v", err)
|
||||||
|
}
|
||||||
|
data = append(data, '\n')
|
||||||
|
_, err = c.rwc.Write(data)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *isolatedIOConn) Close() error {
|
||||||
|
c.closeOnce.Do(func() {
|
||||||
|
c.closeErr = c.rwc.Close()
|
||||||
|
close(c.closed)
|
||||||
|
})
|
||||||
|
return c.closeErr
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ sdkmcp.Transport = (*isolatedCommandTransport)(nil)
|
||||||
|
var _ sdkmcp.Connection = (*isolatedIOConn)(nil)
|
||||||
|
|
@ -372,7 +372,7 @@ func (m *Manager) ConnectServer(
|
||||||
return fmt.Errorf("prepare stdio MCP isolation: %w", err)
|
return fmt.Errorf("prepare stdio MCP isolation: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
transport = &mcp.CommandTransport{Command: cmd}
|
transport = &isolatedCommandTransport{Command: cmd}
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"unsupported transport type: %s (supported: stdio, sse, http)",
|
"unsupported transport type: %s (supported: stdio, sse, http)",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue