smart_box 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmartBox
4
+ module Modes
5
+ class GitWorktreeMode
6
+ attr_reader :source_path, :workspace_path, :branch_name
7
+
8
+ def initialize(source_path:, workspace_path:, box_id:)
9
+ @source_path = File.expand_path(source_path)
10
+ @workspace_path = File.expand_path(workspace_path)
11
+ @box_id = box_id
12
+ @branch_name = "smart-box/#{box_id}"
13
+ end
14
+
15
+ # --- entry points ---
16
+
17
+ def setup
18
+ validate_source!
19
+ ensure_branch!
20
+ create_worktree!
21
+ init_checkpoint!
22
+ end
23
+
24
+ def teardown
25
+ remove_worktree! if worktree_exists?
26
+ end
27
+
28
+ private
29
+
30
+ def validate_source!
31
+ unless Dir.exist?(@source_path)
32
+ raise SmartBox::Error, "Source directory not found: #{@source_path}"
33
+ end
34
+
35
+ unless Dir.exist?(File.join(@source_path, ".git"))
36
+ raise SmartBox::Error, "Source is not a git repository. git-worktree mode requires a git repo."
37
+ end
38
+
39
+ # Check if source is clean
40
+ unless source_clean?
41
+ raise DirtySourceError,
42
+ "Source project has uncommitted changes.\n" \
43
+ "Commit or stash your changes before creating a git-worktree box, " \
44
+ "or use copy mode instead."
45
+ end
46
+ end
47
+
48
+ def source_clean?
49
+ Dir.chdir(@source_path) do
50
+ `git status --porcelain 2>/dev/null`.strip.empty?
51
+ end
52
+ end
53
+
54
+ def ensure_branch!
55
+ Dir.chdir(@source_path) do
56
+ branches = `git branch --list #{@branch_name} 2>/dev/null`.strip
57
+ if branches.empty?
58
+ system("git", "branch", @branch_name, out: File::NULL, err: File::NULL)
59
+ end
60
+ end
61
+ end
62
+
63
+ def create_worktree!
64
+ Dir.chdir(@source_path) do
65
+ system("git", "worktree", "add", @workspace_path, @branch_name,
66
+ out: File::NULL, err: File::NULL)
67
+ end
68
+
69
+ unless Dir.exist?(@workspace_path)
70
+ raise SmartBox::Error, "Failed to create git worktree at #{@workspace_path}"
71
+ end
72
+ end
73
+
74
+ def init_checkpoint!
75
+ Dir.chdir(@workspace_path) do
76
+ system("git", "add", "-A", out: File::NULL, err: File::NULL)
77
+ system("git", "commit", "--allow-empty", "-m", "smart_box initial checkpoint",
78
+ out: File::NULL, err: File::NULL)
79
+ end
80
+ end
81
+
82
+ def remove_worktree!
83
+ Dir.chdir(@source_path) do
84
+ system("git", "worktree", "remove", @workspace_path, "--force",
85
+ out: File::NULL, err: File::NULL)
86
+ system("git", "branch", "-D", @branch_name,
87
+ out: File::NULL, err: File::NULL)
88
+ end
89
+ end
90
+
91
+ def worktree_exists?
92
+ Dir.chdir(@source_path) do
93
+ worktrees = `git worktree list --porcelain 2>/dev/null`
94
+ worktrees.include?(@workspace_path)
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "json"
5
+ require "time"
6
+ require "shellwords"
7
+ require_relative "command_result"
8
+ require_relative "errors"
9
+
10
+ module SmartBox
11
+ class Runner
12
+ # Simple dangerous command patterns (string matching per design spec §12.1)
13
+ DANGEROUS_PATTERNS = [
14
+ /\brm\s+-rf\s+\//,
15
+ /\bmkfs\b/,
16
+ /\bdd\s+if=/,
17
+ /\bshutdown\b/,
18
+ /\breboot\b/,
19
+ /\bsudo\b/,
20
+ /\bchmod\s+-R\s+777\s+\//,
21
+ /\bchown\s+-R\b/
22
+ ].freeze
23
+
24
+ attr_reader :box_id, :workspace_path, :logs_dir
25
+
26
+ def initialize(box_id:, workspace_path:, logs_dir:)
27
+ @box_id = box_id
28
+ @workspace_path = File.expand_path(workspace_path)
29
+ @logs_dir = File.expand_path(logs_dir)
30
+ @command_index = 0
31
+ end
32
+
33
+ # --- main entry point ---
34
+
35
+ def run(command, env: {}, timeout: nil, allow_dangerous: false)
36
+ unless allow_dangerous
37
+ check_dangerous!(command)
38
+ end
39
+
40
+ started_at = Time.now
41
+ @command_index += 1
42
+ idx = @command_index
43
+
44
+ result = execute_command(command, env, timeout, started_at, idx)
45
+ write_logs(command, result, idx)
46
+
47
+ result
48
+ end
49
+
50
+ private
51
+
52
+ def check_dangerous!(command)
53
+ DANGEROUS_PATTERNS.each do |pattern|
54
+ if command.match?(pattern)
55
+ raise DangerousCommandError,
56
+ "Dangerous command detected: #{command.inspect}\n" \
57
+ "Use --allow-dangerous to bypass this check."
58
+ end
59
+ end
60
+ end
61
+
62
+ def execute_command(command, env, timeout, started_at, idx)
63
+ stdout_path = File.join(@logs_dir, "#{format('%04d', idx)}.stdout.log")
64
+ stderr_path = File.join(@logs_dir, "#{format('%04d', idx)}.stderr.log")
65
+
66
+ FileUtils.mkdir_p(@logs_dir)
67
+
68
+ stdout = ""
69
+ stderr = ""
70
+ exit_code = nil
71
+
72
+ Dir.chdir(@workspace_path) do
73
+ args = Shellwords.split(command)
74
+ if env && !env.empty?
75
+ stdout, stderr, status = Open3.capture3(env, *args, chdir: @workspace_path)
76
+ else
77
+ stdout, stderr, status = Open3.capture3(*args, chdir: @workspace_path)
78
+ end
79
+ exit_code = status.exitstatus
80
+ rescue Errno::ENOENT => e
81
+ stderr = "Command not found: #{e.message}"
82
+ exit_code = 127
83
+ end
84
+
85
+ ended_at = Time.now
86
+
87
+ File.write(stdout_path, stdout)
88
+ File.write(stderr_path, stderr)
89
+
90
+ CommandResult.new(
91
+ command: command,
92
+ cwd: @workspace_path,
93
+ stdout: stdout,
94
+ stderr: stderr,
95
+ exit_code: exit_code,
96
+ started_at: started_at,
97
+ ended_at: ended_at
98
+ )
99
+ end
100
+
101
+ def write_logs(command, result, idx)
102
+ write_human_log(command, result)
103
+ write_jsonl_log(command, result, idx)
104
+ end
105
+
106
+ def write_human_log(command, result)
107
+ log_path = File.join(@logs_dir, "commands.log")
108
+ FileUtils.mkdir_p(@logs_dir)
109
+
110
+ entry = <<~ENTRY
111
+ [#{result.started_at.strftime('%Y-%m-%d %H:%M:%S')}] RUN #{command}
112
+ cwd: #{result.cwd}
113
+ exit_code: #{result.exit_code}
114
+
115
+ STDOUT:
116
+ #{result.stdout.strip.empty? ? '(empty)' : result.stdout.strip}
117
+
118
+ STDERR:
119
+ #{result.stderr.strip.empty? ? '(empty)' : result.stderr.strip}
120
+
121
+ ENTRY
122
+
123
+ File.open(log_path, "a") { |f| f.write(entry) }
124
+ end
125
+
126
+ def write_jsonl_log(command, result, idx)
127
+ log_path = File.join(@logs_dir, "commands.jsonl")
128
+ FileUtils.mkdir_p(@logs_dir)
129
+
130
+ entry = {
131
+ "time" => result.started_at.iso8601,
132
+ "box_id" => @box_id,
133
+ "event" => "command_finished",
134
+ "command" => command,
135
+ "cwd" => result.cwd,
136
+ "exit_code" => result.exit_code,
137
+ "elapsed_ms" => result.elapsed_ms,
138
+ "stdout_path" => "logs/#{format('%04d', idx)}.stdout.log",
139
+ "stderr_path" => "logs/#{format('%04d', idx)}.stderr.log"
140
+ }
141
+
142
+ File.open(log_path, "a") { |f| f.puts(JSON.generate(entry)) }
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,3 @@
1
+ module SmartBox
2
+ VERSION = "0.1.1"
3
+ end
data/lib/smart_box.rb ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "smart_box/version"
4
+ require_relative "smart_box/errors"
5
+ require_relative "smart_box/metadata"
6
+ require_relative "smart_box/command_result"
7
+ require_relative "smart_box/runner"
8
+ require_relative "smart_box/modes/copy_mode"
9
+ require_relative "smart_box/modes/git_worktree_mode"
10
+ require_relative "smart_box/box"
metadata ADDED
@@ -0,0 +1,56 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: smart_box
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - SmartBox Team
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: |
13
+ smart_box is a local sandbox system for Coding Agents. It allows agents to
14
+ perform file modifications, command execution, dependency installation,
15
+ experimental fixes, code generation, diff viewing, checkpoint rollback,
16
+ and patch export without directly polluting the real project directory.
17
+ executables:
18
+ - smart_box
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - README.md
23
+ - bin/smart_box
24
+ - lib/smart_box.rb
25
+ - lib/smart_box/box.rb
26
+ - lib/smart_box/cli.rb
27
+ - lib/smart_box/command_result.rb
28
+ - lib/smart_box/errors.rb
29
+ - lib/smart_box/metadata.rb
30
+ - lib/smart_box/modes/copy_mode.rb
31
+ - lib/smart_box/modes/git_worktree_mode.rb
32
+ - lib/smart_box/runner.rb
33
+ - lib/smart_box/version.rb
34
+ licenses:
35
+ - MIT
36
+ metadata:
37
+ source_code_uri: https://github.com/smart-box/smart_box
38
+ changelog_uri: https://github.com/smart-box/smart_box/blob/main/CHANGELOG.md
39
+ rdoc_options: []
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 3.0.0
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ requirements: []
53
+ rubygems_version: 4.0.13
54
+ specification_version: 4
55
+ summary: A local reversible sandbox system for agent task execution
56
+ test_files: []