 8d9b62daf3
			
		
	
	8d9b62daf3
	
	
	
		
			
			This commit implements Phase 2 of the CHORUS Task Execution Engine development plan, providing a comprehensive execution environment abstraction layer with Docker container sandboxing support. ## New Features ### Core Sandbox Interface - Comprehensive ExecutionSandbox interface with isolated task execution - Support for command execution, file I/O, environment management - Resource usage monitoring and sandbox lifecycle management - Standardized error handling with SandboxError types and categories ### Docker Container Sandbox Implementation - Full Docker API integration with secure container creation - Transparent repository mounting with configurable read/write access - Advanced security policies with capability dropping and privilege controls - Comprehensive resource limits (CPU, memory, disk, processes, file handles) - Support for tmpfs mounts, masked paths, and read-only bind mounts - Container lifecycle management with proper cleanup and health monitoring ### Security & Resource Management - Configurable security policies with SELinux, AppArmor, and Seccomp support - Fine-grained capability management with secure defaults - Network isolation options with configurable DNS and proxy settings - Resource monitoring with real-time CPU, memory, and network usage tracking - Comprehensive ulimits configuration for process and file handle limits ### Repository Integration - Seamless repository mounting from local paths to container workspaces - Git configuration support with user credentials and global settings - File inclusion/exclusion patterns for selective repository access - Configurable permissions and ownership for mounted repositories ### Testing Infrastructure - Comprehensive test suite with 60+ test cases covering all functionality - Docker integration tests with Alpine Linux containers (skipped in short mode) - Mock sandbox implementation for unit testing without Docker dependencies - Security policy validation tests with read-only filesystem enforcement - Resource usage monitoring and cleanup verification tests ## Technical Details ### Dependencies Added - github.com/docker/docker v28.4.0+incompatible - Docker API client - github.com/docker/go-connections v0.6.0 - Docker connection utilities - github.com/docker/go-units v0.5.0 - Docker units and formatting - Associated Docker API dependencies for complete container management ### Architecture - Interface-driven design enabling multiple sandbox implementations - Comprehensive configuration structures for all sandbox aspects - Resource usage tracking with detailed metrics collection - Error handling with retryable error classification - Proper cleanup and resource management throughout sandbox lifecycle ### Compatibility - Maintains backward compatibility with existing CHORUS architecture - Designed for future integration with Phase 3 Core Task Execution Engine - Extensible design supporting additional sandbox implementations (VM, process) This Phase 2 implementation provides the foundation for secure, isolated task execution that will be integrated with the AI model providers from Phase 1 in the upcoming Phase 3 development. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
		
			
				
	
	
		
			415 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			415 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2018 The Go Authors. All rights reserved.
 | ||
| // Use of this source code is governed by a BSD-style
 | ||
| // license that can be found in the LICENSE file.
 | ||
| 
 | ||
| // Package descfmt provides functionality to format descriptors.
 | ||
| package descfmt
 | ||
| 
 | ||
| import (
 | ||
| 	"fmt"
 | ||
| 	"io"
 | ||
| 	"reflect"
 | ||
| 	"strconv"
 | ||
| 	"strings"
 | ||
| 
 | ||
| 	"google.golang.org/protobuf/internal/detrand"
 | ||
| 	"google.golang.org/protobuf/internal/pragma"
 | ||
| 	"google.golang.org/protobuf/reflect/protoreflect"
 | ||
| )
 | ||
| 
 | ||
| type list interface {
 | ||
| 	Len() int
 | ||
| 	pragma.DoNotImplement
 | ||
| }
 | ||
| 
 | ||
| func FormatList(s fmt.State, r rune, vs list) {
 | ||
| 	io.WriteString(s, formatListOpt(vs, true, r == 'v' && (s.Flag('+') || s.Flag('#'))))
 | ||
| }
 | ||
| func formatListOpt(vs list, isRoot, allowMulti bool) string {
 | ||
| 	start, end := "[", "]"
 | ||
| 	if isRoot {
 | ||
| 		var name string
 | ||
| 		switch vs.(type) {
 | ||
| 		case protoreflect.Names:
 | ||
| 			name = "Names"
 | ||
| 		case protoreflect.FieldNumbers:
 | ||
| 			name = "FieldNumbers"
 | ||
| 		case protoreflect.FieldRanges:
 | ||
| 			name = "FieldRanges"
 | ||
| 		case protoreflect.EnumRanges:
 | ||
| 			name = "EnumRanges"
 | ||
| 		case protoreflect.FileImports:
 | ||
| 			name = "FileImports"
 | ||
| 		case protoreflect.Descriptor:
 | ||
| 			name = reflect.ValueOf(vs).MethodByName("Get").Type().Out(0).Name() + "s"
 | ||
| 		default:
 | ||
| 			name = reflect.ValueOf(vs).Elem().Type().Name()
 | ||
| 		}
 | ||
| 		start, end = name+"{", "}"
 | ||
| 	}
 | ||
| 
 | ||
| 	var ss []string
 | ||
| 	switch vs := vs.(type) {
 | ||
| 	case protoreflect.Names:
 | ||
| 		for i := 0; i < vs.Len(); i++ {
 | ||
| 			ss = append(ss, fmt.Sprint(vs.Get(i)))
 | ||
| 		}
 | ||
| 		return start + joinStrings(ss, false) + end
 | ||
| 	case protoreflect.FieldNumbers:
 | ||
| 		for i := 0; i < vs.Len(); i++ {
 | ||
| 			ss = append(ss, fmt.Sprint(vs.Get(i)))
 | ||
| 		}
 | ||
| 		return start + joinStrings(ss, false) + end
 | ||
| 	case protoreflect.FieldRanges:
 | ||
| 		for i := 0; i < vs.Len(); i++ {
 | ||
| 			r := vs.Get(i)
 | ||
| 			if r[0]+1 == r[1] {
 | ||
| 				ss = append(ss, fmt.Sprintf("%d", r[0]))
 | ||
| 			} else {
 | ||
| 				ss = append(ss, fmt.Sprintf("%d:%d", r[0], r[1])) // enum ranges are end exclusive
 | ||
| 			}
 | ||
| 		}
 | ||
| 		return start + joinStrings(ss, false) + end
 | ||
| 	case protoreflect.EnumRanges:
 | ||
| 		for i := 0; i < vs.Len(); i++ {
 | ||
| 			r := vs.Get(i)
 | ||
| 			if r[0] == r[1] {
 | ||
| 				ss = append(ss, fmt.Sprintf("%d", r[0]))
 | ||
| 			} else {
 | ||
| 				ss = append(ss, fmt.Sprintf("%d:%d", r[0], int64(r[1])+1)) // enum ranges are end inclusive
 | ||
| 			}
 | ||
| 		}
 | ||
| 		return start + joinStrings(ss, false) + end
 | ||
| 	case protoreflect.FileImports:
 | ||
| 		for i := 0; i < vs.Len(); i++ {
 | ||
| 			var rs records
 | ||
| 			rv := reflect.ValueOf(vs.Get(i))
 | ||
| 			rs.Append(rv, []methodAndName{
 | ||
| 				{rv.MethodByName("Path"), "Path"},
 | ||
| 				{rv.MethodByName("Package"), "Package"},
 | ||
| 				{rv.MethodByName("IsPublic"), "IsPublic"},
 | ||
| 				{rv.MethodByName("IsWeak"), "IsWeak"},
 | ||
| 			}...)
 | ||
| 			ss = append(ss, "{"+rs.Join()+"}")
 | ||
| 		}
 | ||
| 		return start + joinStrings(ss, allowMulti) + end
 | ||
| 	default:
 | ||
| 		_, isEnumValue := vs.(protoreflect.EnumValueDescriptors)
 | ||
| 		for i := 0; i < vs.Len(); i++ {
 | ||
| 			m := reflect.ValueOf(vs).MethodByName("Get")
 | ||
| 			v := m.Call([]reflect.Value{reflect.ValueOf(i)})[0].Interface()
 | ||
| 			ss = append(ss, formatDescOpt(v.(protoreflect.Descriptor), false, allowMulti && !isEnumValue, nil))
 | ||
| 		}
 | ||
| 		return start + joinStrings(ss, allowMulti && isEnumValue) + end
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| type methodAndName struct {
 | ||
| 	method reflect.Value
 | ||
| 	name   string
 | ||
| }
 | ||
| 
 | ||
| func FormatDesc(s fmt.State, r rune, t protoreflect.Descriptor) {
 | ||
| 	io.WriteString(s, formatDescOpt(t, true, r == 'v' && (s.Flag('+') || s.Flag('#')), nil))
 | ||
| }
 | ||
| 
 | ||
| func InternalFormatDescOptForTesting(t protoreflect.Descriptor, isRoot, allowMulti bool, record func(string)) string {
 | ||
| 	return formatDescOpt(t, isRoot, allowMulti, record)
 | ||
| }
 | ||
| 
 | ||
| func formatDescOpt(t protoreflect.Descriptor, isRoot, allowMulti bool, record func(string)) string {
 | ||
| 	rv := reflect.ValueOf(t)
 | ||
| 	rt := rv.MethodByName("ProtoType").Type().In(0)
 | ||
| 
 | ||
| 	start, end := "{", "}"
 | ||
| 	if isRoot {
 | ||
| 		start = rt.Name() + "{"
 | ||
| 	}
 | ||
| 
 | ||
| 	_, isFile := t.(protoreflect.FileDescriptor)
 | ||
| 	rs := records{
 | ||
| 		allowMulti: allowMulti,
 | ||
| 		record:     record,
 | ||
| 	}
 | ||
| 	if t.IsPlaceholder() {
 | ||
| 		if isFile {
 | ||
| 			rs.Append(rv, []methodAndName{
 | ||
| 				{rv.MethodByName("Path"), "Path"},
 | ||
| 				{rv.MethodByName("Package"), "Package"},
 | ||
| 				{rv.MethodByName("IsPlaceholder"), "IsPlaceholder"},
 | ||
| 			}...)
 | ||
| 		} else {
 | ||
| 			rs.Append(rv, []methodAndName{
 | ||
| 				{rv.MethodByName("FullName"), "FullName"},
 | ||
| 				{rv.MethodByName("IsPlaceholder"), "IsPlaceholder"},
 | ||
| 			}...)
 | ||
| 		}
 | ||
| 	} else {
 | ||
| 		switch {
 | ||
| 		case isFile:
 | ||
| 			rs.Append(rv, methodAndName{rv.MethodByName("Syntax"), "Syntax"})
 | ||
| 		case isRoot:
 | ||
| 			rs.Append(rv, []methodAndName{
 | ||
| 				{rv.MethodByName("Syntax"), "Syntax"},
 | ||
| 				{rv.MethodByName("FullName"), "FullName"},
 | ||
| 			}...)
 | ||
| 		default:
 | ||
| 			rs.Append(rv, methodAndName{rv.MethodByName("Name"), "Name"})
 | ||
| 		}
 | ||
| 		switch t := t.(type) {
 | ||
| 		case protoreflect.FieldDescriptor:
 | ||
| 			accessors := []methodAndName{
 | ||
| 				{rv.MethodByName("Number"), "Number"},
 | ||
| 				{rv.MethodByName("Cardinality"), "Cardinality"},
 | ||
| 				{rv.MethodByName("Kind"), "Kind"},
 | ||
| 				{rv.MethodByName("HasJSONName"), "HasJSONName"},
 | ||
| 				{rv.MethodByName("JSONName"), "JSONName"},
 | ||
| 				{rv.MethodByName("HasPresence"), "HasPresence"},
 | ||
| 				{rv.MethodByName("IsExtension"), "IsExtension"},
 | ||
| 				{rv.MethodByName("IsPacked"), "IsPacked"},
 | ||
| 				{rv.MethodByName("IsWeak"), "IsWeak"},
 | ||
| 				{rv.MethodByName("IsList"), "IsList"},
 | ||
| 				{rv.MethodByName("IsMap"), "IsMap"},
 | ||
| 				{rv.MethodByName("MapKey"), "MapKey"},
 | ||
| 				{rv.MethodByName("MapValue"), "MapValue"},
 | ||
| 				{rv.MethodByName("HasDefault"), "HasDefault"},
 | ||
| 				{rv.MethodByName("Default"), "Default"},
 | ||
| 				{rv.MethodByName("ContainingOneof"), "ContainingOneof"},
 | ||
| 				{rv.MethodByName("ContainingMessage"), "ContainingMessage"},
 | ||
| 				{rv.MethodByName("Message"), "Message"},
 | ||
| 				{rv.MethodByName("Enum"), "Enum"},
 | ||
| 			}
 | ||
| 			for _, s := range accessors {
 | ||
| 				switch s.name {
 | ||
| 				case "MapKey":
 | ||
| 					if k := t.MapKey(); k != nil {
 | ||
| 						rs.recs = append(rs.recs, [2]string{"MapKey", k.Kind().String()})
 | ||
| 					}
 | ||
| 				case "MapValue":
 | ||
| 					if v := t.MapValue(); v != nil {
 | ||
| 						switch v.Kind() {
 | ||
| 						case protoreflect.EnumKind:
 | ||
| 							rs.AppendRecs("MapValue", [2]string{"MapValue", string(v.Enum().FullName())})
 | ||
| 						case protoreflect.MessageKind, protoreflect.GroupKind:
 | ||
| 							rs.AppendRecs("MapValue", [2]string{"MapValue", string(v.Message().FullName())})
 | ||
| 						default:
 | ||
| 							rs.AppendRecs("MapValue", [2]string{"MapValue", v.Kind().String()})
 | ||
| 						}
 | ||
| 					}
 | ||
| 				case "ContainingOneof":
 | ||
| 					if od := t.ContainingOneof(); od != nil {
 | ||
| 						rs.AppendRecs("ContainingOneof", [2]string{"Oneof", string(od.Name())})
 | ||
| 					}
 | ||
| 				case "ContainingMessage":
 | ||
| 					if t.IsExtension() {
 | ||
| 						rs.AppendRecs("ContainingMessage", [2]string{"Extendee", string(t.ContainingMessage().FullName())})
 | ||
| 					}
 | ||
| 				case "Message":
 | ||
| 					if !t.IsMap() {
 | ||
| 						rs.Append(rv, s)
 | ||
| 					}
 | ||
| 				default:
 | ||
| 					rs.Append(rv, s)
 | ||
| 				}
 | ||
| 			}
 | ||
| 		case protoreflect.OneofDescriptor:
 | ||
| 			var ss []string
 | ||
| 			fs := t.Fields()
 | ||
| 			for i := 0; i < fs.Len(); i++ {
 | ||
| 				ss = append(ss, string(fs.Get(i).Name()))
 | ||
| 			}
 | ||
| 			if len(ss) > 0 {
 | ||
| 				rs.AppendRecs("Fields", [2]string{"Fields", "[" + joinStrings(ss, false) + "]"})
 | ||
| 			}
 | ||
| 
 | ||
| 		case protoreflect.FileDescriptor:
 | ||
| 			rs.Append(rv, []methodAndName{
 | ||
| 				{rv.MethodByName("Path"), "Path"},
 | ||
| 				{rv.MethodByName("Package"), "Package"},
 | ||
| 				{rv.MethodByName("Imports"), "Imports"},
 | ||
| 				{rv.MethodByName("Messages"), "Messages"},
 | ||
| 				{rv.MethodByName("Enums"), "Enums"},
 | ||
| 				{rv.MethodByName("Extensions"), "Extensions"},
 | ||
| 				{rv.MethodByName("Services"), "Services"},
 | ||
| 			}...)
 | ||
| 
 | ||
| 		case protoreflect.MessageDescriptor:
 | ||
| 			rs.Append(rv, []methodAndName{
 | ||
| 				{rv.MethodByName("IsMapEntry"), "IsMapEntry"},
 | ||
| 				{rv.MethodByName("Fields"), "Fields"},
 | ||
| 				{rv.MethodByName("Oneofs"), "Oneofs"},
 | ||
| 				{rv.MethodByName("ReservedNames"), "ReservedNames"},
 | ||
| 				{rv.MethodByName("ReservedRanges"), "ReservedRanges"},
 | ||
| 				{rv.MethodByName("RequiredNumbers"), "RequiredNumbers"},
 | ||
| 				{rv.MethodByName("ExtensionRanges"), "ExtensionRanges"},
 | ||
| 				{rv.MethodByName("Messages"), "Messages"},
 | ||
| 				{rv.MethodByName("Enums"), "Enums"},
 | ||
| 				{rv.MethodByName("Extensions"), "Extensions"},
 | ||
| 			}...)
 | ||
| 
 | ||
| 		case protoreflect.EnumDescriptor:
 | ||
| 			rs.Append(rv, []methodAndName{
 | ||
| 				{rv.MethodByName("Values"), "Values"},
 | ||
| 				{rv.MethodByName("ReservedNames"), "ReservedNames"},
 | ||
| 				{rv.MethodByName("ReservedRanges"), "ReservedRanges"},
 | ||
| 				{rv.MethodByName("IsClosed"), "IsClosed"},
 | ||
| 			}...)
 | ||
| 
 | ||
| 		case protoreflect.EnumValueDescriptor:
 | ||
| 			rs.Append(rv, []methodAndName{
 | ||
| 				{rv.MethodByName("Number"), "Number"},
 | ||
| 			}...)
 | ||
| 
 | ||
| 		case protoreflect.ServiceDescriptor:
 | ||
| 			rs.Append(rv, []methodAndName{
 | ||
| 				{rv.MethodByName("Methods"), "Methods"},
 | ||
| 			}...)
 | ||
| 
 | ||
| 		case protoreflect.MethodDescriptor:
 | ||
| 			rs.Append(rv, []methodAndName{
 | ||
| 				{rv.MethodByName("Input"), "Input"},
 | ||
| 				{rv.MethodByName("Output"), "Output"},
 | ||
| 				{rv.MethodByName("IsStreamingClient"), "IsStreamingClient"},
 | ||
| 				{rv.MethodByName("IsStreamingServer"), "IsStreamingServer"},
 | ||
| 			}...)
 | ||
| 		}
 | ||
| 		if m := rv.MethodByName("GoType"); m.IsValid() {
 | ||
| 			rs.Append(rv, methodAndName{m, "GoType"})
 | ||
| 		}
 | ||
| 	}
 | ||
| 	return start + rs.Join() + end
 | ||
| }
 | ||
| 
 | ||
| type records struct {
 | ||
| 	recs       [][2]string
 | ||
| 	allowMulti bool
 | ||
| 
 | ||
| 	// record is a function that will be called for every Append() or
 | ||
| 	// AppendRecs() call, to be used for testing with the
 | ||
| 	// InternalFormatDescOptForTesting function.
 | ||
| 	record func(string)
 | ||
| }
 | ||
| 
 | ||
| func (rs *records) AppendRecs(fieldName string, newRecs [2]string) {
 | ||
| 	if rs.record != nil {
 | ||
| 		rs.record(fieldName)
 | ||
| 	}
 | ||
| 	rs.recs = append(rs.recs, newRecs)
 | ||
| }
 | ||
| 
 | ||
| func (rs *records) Append(v reflect.Value, accessors ...methodAndName) {
 | ||
| 	for _, a := range accessors {
 | ||
| 		if rs.record != nil {
 | ||
| 			rs.record(a.name)
 | ||
| 		}
 | ||
| 		var rv reflect.Value
 | ||
| 		if a.method.IsValid() {
 | ||
| 			rv = a.method.Call(nil)[0]
 | ||
| 		}
 | ||
| 		if v.Kind() == reflect.Struct && !rv.IsValid() {
 | ||
| 			rv = v.FieldByName(a.name)
 | ||
| 		}
 | ||
| 		if !rv.IsValid() {
 | ||
| 			panic(fmt.Sprintf("unknown accessor: %v.%s", v.Type(), a.name))
 | ||
| 		}
 | ||
| 		if _, ok := rv.Interface().(protoreflect.Value); ok {
 | ||
| 			rv = rv.MethodByName("Interface").Call(nil)[0]
 | ||
| 			if !rv.IsNil() {
 | ||
| 				rv = rv.Elem()
 | ||
| 			}
 | ||
| 		}
 | ||
| 
 | ||
| 		// Ignore zero values.
 | ||
| 		var isZero bool
 | ||
| 		switch rv.Kind() {
 | ||
| 		case reflect.Interface, reflect.Slice:
 | ||
| 			isZero = rv.IsNil()
 | ||
| 		case reflect.Bool:
 | ||
| 			isZero = rv.Bool() == false
 | ||
| 		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
 | ||
| 			isZero = rv.Int() == 0
 | ||
| 		case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
 | ||
| 			isZero = rv.Uint() == 0
 | ||
| 		case reflect.String:
 | ||
| 			isZero = rv.String() == ""
 | ||
| 		}
 | ||
| 		if n, ok := rv.Interface().(list); ok {
 | ||
| 			isZero = n.Len() == 0
 | ||
| 		}
 | ||
| 		if isZero {
 | ||
| 			continue
 | ||
| 		}
 | ||
| 
 | ||
| 		// Format the value.
 | ||
| 		var s string
 | ||
| 		v := rv.Interface()
 | ||
| 		switch v := v.(type) {
 | ||
| 		case list:
 | ||
| 			s = formatListOpt(v, false, rs.allowMulti)
 | ||
| 		case protoreflect.FieldDescriptor, protoreflect.OneofDescriptor, protoreflect.EnumValueDescriptor, protoreflect.MethodDescriptor:
 | ||
| 			s = string(v.(protoreflect.Descriptor).Name())
 | ||
| 		case protoreflect.Descriptor:
 | ||
| 			s = string(v.FullName())
 | ||
| 		case string:
 | ||
| 			s = strconv.Quote(v)
 | ||
| 		case []byte:
 | ||
| 			s = fmt.Sprintf("%q", v)
 | ||
| 		default:
 | ||
| 			s = fmt.Sprint(v)
 | ||
| 		}
 | ||
| 		rs.recs = append(rs.recs, [2]string{a.name, s})
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| func (rs *records) Join() string {
 | ||
| 	var ss []string
 | ||
| 
 | ||
| 	// In single line mode, simply join all records with commas.
 | ||
| 	if !rs.allowMulti {
 | ||
| 		for _, r := range rs.recs {
 | ||
| 			ss = append(ss, r[0]+formatColon(0)+r[1])
 | ||
| 		}
 | ||
| 		return joinStrings(ss, false)
 | ||
| 	}
 | ||
| 
 | ||
| 	// In allowMulti line mode, align single line records for more readable output.
 | ||
| 	var maxLen int
 | ||
| 	flush := func(i int) {
 | ||
| 		for _, r := range rs.recs[len(ss):i] {
 | ||
| 			ss = append(ss, r[0]+formatColon(maxLen-len(r[0]))+r[1])
 | ||
| 		}
 | ||
| 		maxLen = 0
 | ||
| 	}
 | ||
| 	for i, r := range rs.recs {
 | ||
| 		if isMulti := strings.Contains(r[1], "\n"); isMulti {
 | ||
| 			flush(i)
 | ||
| 			ss = append(ss, r[0]+formatColon(0)+strings.Join(strings.Split(r[1], "\n"), "\n\t"))
 | ||
| 		} else if maxLen < len(r[0]) {
 | ||
| 			maxLen = len(r[0])
 | ||
| 		}
 | ||
| 	}
 | ||
| 	flush(len(rs.recs))
 | ||
| 	return joinStrings(ss, true)
 | ||
| }
 | ||
| 
 | ||
| func formatColon(padding int) string {
 | ||
| 	// Deliberately introduce instability into the debug output to
 | ||
| 	// discourage users from performing string comparisons.
 | ||
| 	// This provides us flexibility to change the output in the future.
 | ||
| 	if detrand.Bool() {
 | ||
| 		return ":" + strings.Repeat(" ", 1+padding) // use non-breaking spaces (U+00a0)
 | ||
| 	} else {
 | ||
| 		return ":" + strings.Repeat(" ", 1+padding) // use regular spaces (U+0020)
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| func joinStrings(ss []string, isMulti bool) string {
 | ||
| 	if len(ss) == 0 {
 | ||
| 		return ""
 | ||
| 	}
 | ||
| 	if isMulti {
 | ||
| 		return "\n\t" + strings.Join(ss, "\n\t") + "\n"
 | ||
| 	}
 | ||
| 	return strings.Join(ss, ", ")
 | ||
| }
 |