Rename role to sandbox in layered sandbox config

- cfg/roles/ → cfg/sandbox/
- DefaultRole → DefaultSandbox, LoadRole → LoadSandbox
- SessionOptions.Role → Sandbox, Session.Role() → Sandbox()
- p9 file: role → sandbox
- Remove tasks from layered sandbox config
- Update create_session.sh and SKILL.md docs
This commit is contained in:
Levi Neely 2026-03-04 13:10:10 +01:00
parent c93bd19487
commit d0e3ff2cd8
8 changed files with 49 additions and 69 deletions

View File

@ -107,5 +107,7 @@ env:
- BITBUCKET_TOKEN
- BITBUCKET_USERNAME
- PCLOUD_USER
- EDITOR
- GIT_SEQUENCE_EDITOR
network:
unrestricted: true

View File

@ -15,10 +15,9 @@ var (
// SessionOptions contains options for creating a session
type SessionOptions struct {
CWD string
Role string
Tasks []string
Model string // Optional model override (empty = backend default)
CWD string
Sandbox string
Model string // Optional model override (empty = backend default)
}
// Backend represents any chat backend (CLI tool via PTY, or direct API)
@ -74,11 +73,8 @@ type Session interface {
// SetAlias sets a user-friendly alias for the session
SetAlias(alias string)
// Role returns the session role (empty if not specified)
Role() string
// Tasks returns the session tasks (empty if not specified)
Tasks() []string
// Sandbox returns the session sandbox config (empty if not specified)
Sandbox() string
// Model returns the active model override (empty = backend default)
Model() string

View File

@ -26,9 +26,8 @@ type Session struct {
windowName string // window name within tmux session (same as id)
cwd string
alias string
role string // session role (e.g., "developer", "reviewer")
tasks []string // session tasks (e.g., ["git", "docker"])
winID int // Acme window ID (0 if not applicable)
sandbox string // sandbox config (e.g., "default", "restricted")
winID int // Acme window ID (0 if not applicable)
pid int
state string
context string // injected into first prompt only (consumed on first Send)
@ -189,18 +188,11 @@ func (s *Session) Commands() backend.CommandHandler {
return s.commands
}
// Role returns the session role
func (s *Session) Role() string {
// Sandbox returns the session sandbox config
func (s *Session) Sandbox() string {
s.mu.Lock()
defer s.mu.Unlock()
return s.role
}
// Tasks returns the session tasks
func (s *Session) Tasks() []string {
s.mu.Lock()
defer s.mu.Unlock()
return s.tasks
return s.sandbox
}
// Model returns the active model override (empty = backend default)
@ -670,7 +662,7 @@ func (s *Session) Restart(ctx context.Context) error {
environment := s.environment
backendName := s.backendName
backendCommand := s.backendCommand
role := s.role
sbx := s.sandbox
hadCrash := s.hadCrash
model := s.model
modelResolver := s.modelResolver
@ -711,14 +703,14 @@ func (s *Session) Restart(ctx context.Context) error {
}
layers = append(layers, backendLayer)
if role == "" {
role = sandbox.DefaultRole
if sbx == "" {
sbx = sandbox.DefaultSandbox
}
roleLayer, err := sandbox.LoadRole(role)
sbxLayer, err := sandbox.LoadSandbox(sbx)
if err != nil {
return fmt.Errorf("failed to load role %q: %w", role, err)
return fmt.Errorf("failed to load sandbox %q: %w", sbx, err)
}
layers = append(layers, roleLayer)
layers = append(layers, sbxLayer)
general := sandbox.GeneralConfig{BestEffort: false, LogLevel: "error"}
advanced := sandbox.AdvancedConfig{LDD: false, AddExec: true}

View File

@ -125,7 +125,7 @@ func (b *Backend) CreateSession(ctx context.Context, opts backend.SessionOptions
id := generateID()
windowName := id // Use session ID as window name
debug.Log("[session %s] creating window in tmux session %s (role=%s, tasks=%v)", id, b.tmuxSession, opts.Role, opts.Tasks)
debug.Log("[session %s] creating window in tmux session %s (sandbox=%s)", id, b.tmuxSession, opts.Sandbox)
// Build layered sandbox configuration
// Start with global.yaml as base
@ -148,16 +148,16 @@ func (b *Backend) CreateSession(ctx context.Context, opts backend.SessionOptions
}
layers = append(layers, backendLayer)
// Add role layer if specified, otherwise use "default" role
role := opts.Role
if role == "" {
role = sandbox.DefaultRole
// Add sandbox layer if specified, otherwise use "default"
sbx := opts.Sandbox
if sbx == "" {
sbx = sandbox.DefaultSandbox
}
roleLayer, err := sandbox.LoadRole(role)
sbxLayer, err := sandbox.LoadSandbox(sbx)
if err != nil {
return nil, fmt.Errorf("failed to load role %q: %w", role, err)
return nil, fmt.Errorf("failed to load sandbox %q: %w", sbx, err)
}
layers = append(layers, roleLayer)
layers = append(layers, sbxLayer)
// Merge layers into final config
general := sandbox.GeneralConfig{
@ -348,8 +348,7 @@ func (b *Backend) CreateSession(ctx context.Context, opts backend.SessionOptions
tmuxSession: b.tmuxSession,
windowName: windowName,
cwd: opts.CWD,
role: role,
tasks: opts.Tasks,
sandbox: sbx,
model: opts.Model,
modelResolver: b.cfg.ModelResolver,
pid: pid,

View File

@ -105,15 +105,14 @@ const (
fileAlias
fileBackend
fileContext
fileRole
fileTasks
fileSandbox
fileTmux
fileMail
fileModel
fileCount
)
var fileNames = []string{"ctl", "state", "pid", "cwd", "alias", "backend", "context", "role", "tasks", "tmux", "mail", "model"}
var fileNames = []string{"ctl", "state", "pid", "cwd", "alias", "backend", "context", "sandbox", "tmux", "mail", "model"}
// Directory names in session
var dirNames = []string{"inbox", "outbox", "completed"}
@ -700,7 +699,7 @@ func (s *Server) write(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
if path == "/ctl" {
args := strings.Fields(input)
if len(args) < 2 || args[0] != "new" {
return errFcall(fc, "usage: new <backend> <cwd> [role=<role>] [tasks=<task1,task2>]")
return errFcall(fc, "usage: new <backend> <cwd> [sandbox=<sandbox>] [model=<model>]")
}
backendName := args[1]
@ -708,20 +707,15 @@ func (s *Server) write(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
if err != nil {
return errFcall(fc, fmt.Sprintf("failed to get working directory: %v", err))
}
var role string
var tasks []string
var sbx string
var model string
// Parse remaining arguments: first non-key=value is cwd, rest are options
cwdSet := false
for i := 2; i < len(args); i++ {
arg := args[i]
if r, ok := strings.CutPrefix(arg, "role="); ok {
role = r
} else if t, ok := strings.CutPrefix(arg, "tasks="); ok {
if t != "" {
tasks = strings.Split(t, ",")
}
if s, ok := strings.CutPrefix(arg, "sandbox="); ok {
sbx = s
} else if m, ok := strings.CutPrefix(arg, "model="); ok {
model = m
} else if !cwdSet {
@ -753,10 +747,9 @@ func (s *Server) write(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
}
opts := backend.SessionOptions{
CWD: cleanPath,
Role: role,
Tasks: tasks,
Model: model,
CWD: cleanPath,
Sandbox: sbx,
Model: model,
}
_, err = s.mgr.New(opts, backendName)
if err != nil {
@ -1550,10 +1543,8 @@ func (s *Server) getSessionFile(sess backend.Session, idx int) string {
return tmuxSess.GetContext()
}
return ""
case fileRole:
return sess.Role()
case fileTasks:
return strings.Join(sess.Tasks(), ",")
case fileSandbox:
return sess.Sandbox()
case fileTmux:
tmuxSession, hasSession := meta.Extra["tmux_session"]
tmuxWindow, hasWindow := meta.Extra["tmux_window"]

View File

@ -176,8 +176,8 @@ func Save(cfg *Config) error {
return os.WriteFile(path, data, 0644)
}
// DefaultRole is the role used when none is specified
const DefaultRole = "default"
// DefaultSandbox is the sandbox config used when none is specified
const DefaultSandbox = "default"
// validateName checks if a role/task name is safe for use in file paths
func validateName(name string) error {
@ -344,18 +344,18 @@ func LoadBackend(name string) (LayeredConfig, error) {
return cfg, nil
}
// LoadRole loads role-specific config
func LoadRole(name string) (LayeredConfig, error) {
// LoadSandbox loads sandbox-specific config
func LoadSandbox(name string) (LayeredConfig, error) {
if err := validateName(name); err != nil {
return LayeredConfig{}, fmt.Errorf("invalid role name: %w", err)
return LayeredConfig{}, fmt.Errorf("invalid sandbox name: %w", err)
}
home, _ := os.UserHomeDir()
path := filepath.Join(home, ".config", "anvillm", "roles", name+".yaml")
path := filepath.Join(home, ".config", "anvillm", "sandbox", name+".yaml")
data, err := os.ReadFile(path)
if os.IsNotExist(err) {
return LayeredConfig{}, fmt.Errorf("role %q not found (looked in %s)", name, path)
return LayeredConfig{}, fmt.Errorf("sandbox %q not found (looked in %s)", name, path)
}
if err != nil {
return LayeredConfig{}, err

View File

@ -1,7 +1,7 @@
#!/bin/bash
# capabilities: agents
# description: Create a new agent session
# Usage: create_session.sh <backend> <cwd> [role=<role>] [tasks=<task1,task2>] [model=<model>]
# Usage: create_session.sh <backend> <cwd> [sandbox=<sandbox>] [model=<model>]
set -euo pipefail
if cat /etc/shadow >/dev/null 2>&1; then
@ -10,7 +10,7 @@ if cat /etc/shadow >/dev/null 2>&1; then
fi
if [ $# -lt 2 ]; then
echo "usage: create_session.sh <backend> <cwd> [role=<role>] [tasks=<task1,task2>] [model=<model>]" >&2
echo "usage: create_session.sh <backend> <cwd> [sandbox=<sandbox>] [model=<model>]" >&2
exit 1
fi

View File

@ -13,7 +13,7 @@ Create and control agent sessions.
All commands must be run via `execute_code` tool:
- List sessions: `list_sessions.sh`
- Create session: `create_session.sh <backend> <cwd> [role=<role>] [tasks=<task1,task2>] [model=<model>]`
- Create session: `create_session.sh <backend> <cwd> [sandbox=<sandbox>] [model=<model>]`
- Control session: `control_session.sh <session-id> <command>`
- Spawn agent: `spawn_agent.sh <agent-id> [cwd-path] [initial-context-prompt]`
- Kill agent: `kill_agent.sh [cwd-path]`
@ -26,7 +26,7 @@ All commands must be run via `execute_code` tool:
## Session Lifecycle
1. Create: `create_session.sh kiro-cli /path/to/project role=developer`
1. Create: `create_session.sh kiro-cli /path/to/project sandbox=default`
2. Monitor: `list_sessions.sh` (check state field)
3. Control: `control_session.sh <id> stop|restart|kill|refresh`