chocomint 1.0.0
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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +224 -0
- data/THIRD_PARTY_LICENSES.md +45 -0
- data/bin/chocomint +37 -0
- data/bin/chocomint-console-worker +35 -0
- data/bin/chocomint-server +43 -0
- data/config/config.yml +37 -0
- data/lib/chocomint/config.rb +74 -0
- data/lib/chocomint/console/conpty.rb +296 -0
- data/lib/chocomint/console/server.rb +164 -0
- data/lib/chocomint/console/worker.rb +88 -0
- data/lib/chocomint/console/worker_process.rb +254 -0
- data/lib/chocomint/edit_git_handlers.rb +397 -0
- data/lib/chocomint/edit_handlers.rb +696 -0
- data/lib/chocomint/edit_html.rb +9 -0
- data/lib/chocomint/errors.rb +18 -0
- data/lib/chocomint/factory.rb +145 -0
- data/lib/chocomint/llm/base_client.rb +113 -0
- data/lib/chocomint/llm/chat_client.rb +206 -0
- data/lib/chocomint/llm/primary_client.rb +320 -0
- data/lib/chocomint/llm/verifier_client.rb +127 -0
- data/lib/chocomint/logger/sqlite_logger.rb +245 -0
- data/lib/chocomint/orchestrator.rb +175 -0
- data/lib/chocomint/planner.rb +241 -0
- data/lib/chocomint/security/path_guard.rb +84 -0
- data/lib/chocomint/server.rb +863 -0
- data/lib/chocomint/tools/append_file.rb +50 -0
- data/lib/chocomint/tools/base.rb +88 -0
- data/lib/chocomint/tools/delete_file.rb +42 -0
- data/lib/chocomint/tools/edit.rb +65 -0
- data/lib/chocomint/tools/fetch_url.rb +156 -0
- data/lib/chocomint/tools/file_info.rb +54 -0
- data/lib/chocomint/tools/glob.rb +58 -0
- data/lib/chocomint/tools/grep.rb +78 -0
- data/lib/chocomint/tools/list_dir.rb +41 -0
- data/lib/chocomint/tools/ls.rb +51 -0
- data/lib/chocomint/tools/make_dir.rb +44 -0
- data/lib/chocomint/tools/read_file.rb +45 -0
- data/lib/chocomint/tools/registry.rb +29 -0
- data/lib/chocomint/tools/run_command.rb +96 -0
- data/lib/chocomint/tools/shell.rb +169 -0
- data/lib/chocomint/tools/write_file.rb +54 -0
- data/lib/chocomint/validator/machine_validator.rb +65 -0
- data/lib/chocomint/validator/semantic_validator.rb +52 -0
- data/lib/chocomint.rb +51 -0
- data/public/edit/edit.css +414 -0
- data/public/edit/edit.js +2137 -0
- data/public/edit/index.html +251 -0
- data/public/vendor/monaco/LICENSE.txt +23 -0
- data/public/vendor/monaco/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
- data/public/vendor/monaco/vs/base/worker/workerMain.js +31 -0
- data/public/vendor/monaco/vs/basic-languages/abap/abap.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/apex/apex.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/azcli/azcli.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/bat/bat.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/bicep/bicep.js +11 -0
- data/public/vendor/monaco/vs/basic-languages/cameligo/cameligo.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/clojure/clojure.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/coffee/coffee.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/cpp/cpp.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/csharp/csharp.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/csp/csp.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/css/css.js +12 -0
- data/public/vendor/monaco/vs/basic-languages/cypher/cypher.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/dart/dart.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/dockerfile/dockerfile.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/ecl/ecl.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/elixir/elixir.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/flow9/flow9.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/freemarker2/freemarker2.js +12 -0
- data/public/vendor/monaco/vs/basic-languages/fsharp/fsharp.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/go/go.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/graphql/graphql.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/handlebars/handlebars.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/hcl/hcl.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/html/html.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/ini/ini.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/java/java.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/javascript/javascript.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/julia/julia.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/kotlin/kotlin.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/less/less.js +11 -0
- data/public/vendor/monaco/vs/basic-languages/lexon/lexon.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/liquid/liquid.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/lua/lua.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/m3/m3.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/markdown/markdown.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/mdx/mdx.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/mips/mips.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/msdax/msdax.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/mysql/mysql.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/objective-c/objective-c.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/pascal/pascal.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/pascaligo/pascaligo.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/perl/perl.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/pgsql/pgsql.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/php/php.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/pla/pla.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/postiats/postiats.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/powerquery/powerquery.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/powershell/powershell.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/protobuf/protobuf.js +11 -0
- data/public/vendor/monaco/vs/basic-languages/pug/pug.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/python/python.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/qsharp/qsharp.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/r/r.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/razor/razor.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/redis/redis.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/redshift/redshift.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/restructuredtext/restructuredtext.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/ruby/ruby.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/rust/rust.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/sb/sb.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/scala/scala.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/scheme/scheme.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/scss/scss.js +12 -0
- data/public/vendor/monaco/vs/basic-languages/shell/shell.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/solidity/solidity.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/sophia/sophia.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/sparql/sparql.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/sql/sql.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/st/st.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/swift/swift.js +13 -0
- data/public/vendor/monaco/vs/basic-languages/systemverilog/systemverilog.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/tcl/tcl.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/twig/twig.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/typescript/typescript.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/typespec/typespec.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/vb/vb.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/wgsl/wgsl.js +307 -0
- data/public/vendor/monaco/vs/basic-languages/xml/xml.js +10 -0
- data/public/vendor/monaco/vs/basic-languages/yaml/yaml.js +10 -0
- data/public/vendor/monaco/vs/editor/editor.main.css +8 -0
- data/public/vendor/monaco/vs/editor/editor.main.js +798 -0
- data/public/vendor/monaco/vs/language/css/cssMode.js +13 -0
- data/public/vendor/monaco/vs/language/css/cssWorker.js +77 -0
- data/public/vendor/monaco/vs/language/html/htmlMode.js +13 -0
- data/public/vendor/monaco/vs/language/html/htmlWorker.js +454 -0
- data/public/vendor/monaco/vs/language/json/jsonMode.js +19 -0
- data/public/vendor/monaco/vs/language/json/jsonWorker.js +42 -0
- data/public/vendor/monaco/vs/language/typescript/tsMode.js +20 -0
- data/public/vendor/monaco/vs/language/typescript/tsWorker.js +51328 -0
- data/public/vendor/monaco/vs/loader.js +11 -0
- data/public/vendor/monaco/vs/nls.messages.de.js +21 -0
- data/public/vendor/monaco/vs/nls.messages.es.js +21 -0
- data/public/vendor/monaco/vs/nls.messages.fr.js +19 -0
- data/public/vendor/monaco/vs/nls.messages.it.js +19 -0
- data/public/vendor/monaco/vs/nls.messages.ja.js +21 -0
- data/public/vendor/monaco/vs/nls.messages.ko.js +19 -0
- data/public/vendor/monaco/vs/nls.messages.ru.js +21 -0
- data/public/vendor/monaco/vs/nls.messages.zh-cn.js +21 -0
- data/public/vendor/monaco/vs/nls.messages.zh-tw.js +19 -0
- data/public/vendor/xterm/LICENSE +25 -0
- data/public/vendor/xterm/addon-fit.js +2 -0
- data/public/vendor/xterm/xterm.css +218 -0
- data/public/vendor/xterm/xterm.js +2 -0
- metadata +318 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Chocomint
|
|
6
|
+
module Tools
|
|
7
|
+
# 許可ディレクトリ内にディレクトリを作成する (親も含め mkdir_p)。
|
|
8
|
+
class MakeDir < Base
|
|
9
|
+
def initialize(path_guard:)
|
|
10
|
+
@path_guard = path_guard
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def name = "make_dir"
|
|
14
|
+
|
|
15
|
+
def description
|
|
16
|
+
"指定パスにディレクトリを作成する (親ディレクトリも作成)。path は許可ディレクトリ内であること。"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def schema
|
|
20
|
+
{
|
|
21
|
+
"type" => "object",
|
|
22
|
+
"properties" => {
|
|
23
|
+
"path" => { "type" => "string" }
|
|
24
|
+
},
|
|
25
|
+
"required" => %w[path],
|
|
26
|
+
"additionalProperties" => false
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def call(args)
|
|
31
|
+
measure do
|
|
32
|
+
abs = @path_guard.resolve(args["path"])
|
|
33
|
+
if File.file?(abs)
|
|
34
|
+
next fail(stderr: "a file already exists at #{args['path']}")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
require "fileutils"
|
|
38
|
+
FileUtils.mkdir_p(abs)
|
|
39
|
+
ok(stdout: "created directory #{args['path']}")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Chocomint
|
|
6
|
+
module Tools
|
|
7
|
+
# 許可ディレクトリ内のファイルを読み、内容を stdout に返す。
|
|
8
|
+
class ReadFile < Base
|
|
9
|
+
def initialize(path_guard:, max_file_bytes:)
|
|
10
|
+
@path_guard = path_guard
|
|
11
|
+
@max_file_bytes = max_file_bytes
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def name = "read_file"
|
|
15
|
+
|
|
16
|
+
def description
|
|
17
|
+
"指定パスのファイル内容を読み取る。path は許可ディレクトリ内であること。"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def schema
|
|
21
|
+
{
|
|
22
|
+
"type" => "object",
|
|
23
|
+
"properties" => {
|
|
24
|
+
"path" => { "type" => "string" }
|
|
25
|
+
},
|
|
26
|
+
"required" => %w[path],
|
|
27
|
+
"additionalProperties" => false
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def call(args)
|
|
32
|
+
measure do
|
|
33
|
+
abs = @path_guard.resolve(args["path"])
|
|
34
|
+
next fail(stderr: "no such file: #{args['path']}") unless File.file?(abs)
|
|
35
|
+
|
|
36
|
+
if File.size(abs) > @max_file_bytes
|
|
37
|
+
next fail(stderr: "file exceeds max_file_bytes (#{@max_file_bytes})")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
ok(stdout: File.read(abs))
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../errors"
|
|
4
|
+
|
|
5
|
+
module Chocomint
|
|
6
|
+
module Tools
|
|
7
|
+
# ツール名 -> インスタンスの解決 (DESIGN §14: 未知ツールは FAIL)。
|
|
8
|
+
class Registry
|
|
9
|
+
def initialize
|
|
10
|
+
@tools = {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def register(tool)
|
|
14
|
+
@tools[tool.name] = tool
|
|
15
|
+
self
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def fetch(name)
|
|
19
|
+
@tools[name] or raise UnknownToolError, "unknown tool: #{name}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def include?(name) = @tools.key?(name)
|
|
23
|
+
|
|
24
|
+
def all = @tools.values
|
|
25
|
+
|
|
26
|
+
def tool_definitions = all.map(&:to_tool_definition)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require_relative "base"
|
|
5
|
+
require_relative "../errors"
|
|
6
|
+
|
|
7
|
+
module Chocomint
|
|
8
|
+
module Tools
|
|
9
|
+
# whitelist に登録されたコマンドのみを実行する (DESIGN §13)。
|
|
10
|
+
# shell を介さず引数配列で spawn するため、shell 展開・インジェクションは起きない。
|
|
11
|
+
class RunCommand < Base
|
|
12
|
+
def initialize(allowed_commands:, timeout_sec:, max_output_bytes:, cwd: Dir.pwd,
|
|
13
|
+
path_guard: nil, allow_all: false)
|
|
14
|
+
@allowed_commands = Array(allowed_commands)
|
|
15
|
+
@timeout_sec = timeout_sec
|
|
16
|
+
@max_output_bytes = max_output_bytes
|
|
17
|
+
@cwd = cwd
|
|
18
|
+
@path_guard = path_guard
|
|
19
|
+
@allow_all = allow_all
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def name = "run_command"
|
|
23
|
+
|
|
24
|
+
def description
|
|
25
|
+
if @allow_all
|
|
26
|
+
"任意のコマンドを引数付きで実行する (whitelist 無効化 - allow_all_commands: true)。" \
|
|
27
|
+
"shell 展開は行われない (args は個別のトークンとして渡す)。"
|
|
28
|
+
else
|
|
29
|
+
"許可されたコマンドを引数付きで実行する。許可コマンド: #{@allowed_commands.join(', ')}。" \
|
|
30
|
+
"shell 展開は行われない (args は個別のトークンとして渡す)。"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def schema
|
|
35
|
+
{
|
|
36
|
+
"type" => "object",
|
|
37
|
+
"properties" => {
|
|
38
|
+
"command" => { "type" => "string" },
|
|
39
|
+
"args" => {
|
|
40
|
+
"type" => "array",
|
|
41
|
+
"items" => { "type" => "string" }
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"required" => %w[command],
|
|
45
|
+
"additionalProperties" => false
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def call(args)
|
|
50
|
+
measure do
|
|
51
|
+
command = args["command"].to_s
|
|
52
|
+
cmd_args = Array(args["args"]).map(&:to_s)
|
|
53
|
+
|
|
54
|
+
unless @allow_all || @allowed_commands.include?(command)
|
|
55
|
+
next fail(stderr: "command not allowed: #{command}")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# 外部プロセスは cwd 配下で自由に書き込めるため、引数に現れるパスが
|
|
59
|
+
# 許可ディレクトリ内に収まるか事前に検証する (allowed_dirs の隔離を守る)。
|
|
60
|
+
if @path_guard
|
|
61
|
+
begin
|
|
62
|
+
@path_guard.validate_args!(cmd_args)
|
|
63
|
+
rescue PathAccessError => e
|
|
64
|
+
next fail(stderr: e.message)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
run(command, cmd_args)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def run(command, cmd_args)
|
|
75
|
+
stdout, stderr, status =
|
|
76
|
+
Open3.capture3([command, command], *cmd_args, chdir: @cwd)
|
|
77
|
+
|
|
78
|
+
{
|
|
79
|
+
exit_code: status.exitstatus || 1,
|
|
80
|
+
stdout: sanitize(stdout),
|
|
81
|
+
stderr: sanitize(stderr)
|
|
82
|
+
}
|
|
83
|
+
rescue Errno::ENOENT
|
|
84
|
+
fail(stderr: "command not found: #{command}")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# 外部プロセスの出力を「上限まで切り詰め + 妥当な UTF-8 に正規化」する。
|
|
88
|
+
# Open3 の出力は ASCII-8BIT で、Windows の python 等は UTF-8 ではなく CP932 で
|
|
89
|
+
# 日本語を出力するため、UTF-8 誤認で化けないよう to_utf8 で CP932 変換を試みる。
|
|
90
|
+
def sanitize(text)
|
|
91
|
+
text = text.byteslice(0, @max_output_bytes) if text.bytesize > @max_output_bytes
|
|
92
|
+
to_utf8(text)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "timeout"
|
|
5
|
+
require_relative "base"
|
|
6
|
+
|
|
7
|
+
module Chocomint
|
|
8
|
+
module Tools
|
|
9
|
+
# shell を明示的に介してスクリプトを実行する (bash / powershell)。
|
|
10
|
+
#
|
|
11
|
+
# DESIGN §13 の「shell 展開禁止」原則からは外れる強力なツールなので、
|
|
12
|
+
# cwd を base_dir に固定し、timeout と出力サイズ制限を必ず適用する。
|
|
13
|
+
# bash / powershell の 2 種類を launcher で切り替える。
|
|
14
|
+
class Shell < Base
|
|
15
|
+
# Windows では PATH 上の "bash" が WSL インタロップ (system32\bash.exe) に
|
|
16
|
+
# 解決されることがあり、これは -c に渡した複数文スクリプト中の変数展開が
|
|
17
|
+
# 壊れる既知の癖を持つ。Git Bash を優先的に探し、無ければ "bash" にフォールバックする。
|
|
18
|
+
GIT_BASH_CANDIDATES = [
|
|
19
|
+
"C:/Program Files/Git/bin/bash.exe",
|
|
20
|
+
"C:/Program Files (x86)/Git/bin/bash.exe"
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
def self.resolve_bash_exe
|
|
24
|
+
GIT_BASH_CANDIDATES.find { |path| File.exist?(path) } || "bash"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
LAUNCHERS = {
|
|
28
|
+
"bash" => {
|
|
29
|
+
name: "bash",
|
|
30
|
+
exe: resolve_bash_exe,
|
|
31
|
+
argv: ->(script) { ["-c", script] }
|
|
32
|
+
},
|
|
33
|
+
"powershell" => {
|
|
34
|
+
name: "powershell",
|
|
35
|
+
exe: "pwsh",
|
|
36
|
+
argv: ->(script) { ["-NoProfile", "-NonInteractive", "-Command", script] }
|
|
37
|
+
}
|
|
38
|
+
}.freeze
|
|
39
|
+
|
|
40
|
+
def self.build(kind, **opts)
|
|
41
|
+
spec = LAUNCHERS.fetch(kind)
|
|
42
|
+
new(spec: spec, **opts)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def initialize(spec:, timeout_sec:, max_output_bytes:, cwd: Dir.pwd, path_guard: nil)
|
|
46
|
+
@spec = spec
|
|
47
|
+
@timeout_sec = timeout_sec
|
|
48
|
+
@max_output_bytes = max_output_bytes
|
|
49
|
+
@cwd = cwd
|
|
50
|
+
@path_guard = path_guard
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def name = @spec[:name]
|
|
54
|
+
|
|
55
|
+
def description
|
|
56
|
+
"#{@spec[:name]} スクリプトを実行する。作業ディレクトリは固定され、" \
|
|
57
|
+
"timeout #{@timeout_sec}s と出力サイズ制限が適用される。"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def schema
|
|
61
|
+
{
|
|
62
|
+
"type" => "object",
|
|
63
|
+
"properties" => {
|
|
64
|
+
"script" => { "type" => "string" }
|
|
65
|
+
},
|
|
66
|
+
"required" => %w[script],
|
|
67
|
+
"additionalProperties" => false
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def call(args)
|
|
72
|
+
measure do
|
|
73
|
+
script = args["script"].to_s
|
|
74
|
+
if (denied = sandbox_violation(script))
|
|
75
|
+
next fail(stderr: denied)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
run(script)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
# スクリプトは自由記述で完全なトークン分割はできないため、明白な脱出パターン
|
|
85
|
+
# (絶対パス・親ディレクトリ遡り) だけを弾く軽量ガード。run_command の引数検証と
|
|
86
|
+
# 違い網羅は狙わず、workspace 外への露骨な書き込みを防ぐ最低限の防波堤とする。
|
|
87
|
+
# cwd は base_dir (プロジェクトルート) 固定で workspace 外なので、絶対パスと
|
|
88
|
+
# ".." による遡りを疑わしいトークンとして弾く。
|
|
89
|
+
|
|
90
|
+
# scheme://... の URL。絶対パス判定の前に取り除き、http:// を誤検知しない。
|
|
91
|
+
URL = %r{\b[a-zA-Z][a-zA-Z0-9+.\-]*://\S*}
|
|
92
|
+
# Windows ドライブ (C:\ / C:/)、UNC (\\host)、Unix 絶対パス (区切り直後の / で
|
|
93
|
+
# 始まり、ルート単独 "/" も含む)。
|
|
94
|
+
ABSOLUTE_PATH = %r{[A-Za-z]:[\\/]|\\\\[^\s]|(?<=\A|[\s'"=(><|&;])/}
|
|
95
|
+
# "../" もしくは "..\" (先頭が単語構成文字でない = パス区切り直後)。
|
|
96
|
+
PARENT_ESCAPE = %r{(?<![\w.])\.\.[\\/]}
|
|
97
|
+
|
|
98
|
+
def sandbox_violation(script)
|
|
99
|
+
return nil if @path_guard.nil?
|
|
100
|
+
|
|
101
|
+
# URL を除去してから絶対パスを探す (スキームの // を絶対パスと混同しないため)。
|
|
102
|
+
stripped = script.gsub(URL, " ")
|
|
103
|
+
|
|
104
|
+
if stripped.match?(ABSOLUTE_PATH)
|
|
105
|
+
return "absolute paths are not allowed in shell scripts (stay within #{workspace_hint})"
|
|
106
|
+
end
|
|
107
|
+
if stripped.match?(PARENT_ESCAPE)
|
|
108
|
+
return "parent-directory escape (..) is not allowed in shell scripts"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def workspace_hint
|
|
115
|
+
@path_guard.allowed_roots.first
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# 色付き出力を抑止する環境変数。多くの CLI が尊重する慣習 (NO_COLOR / TERM=dumb)。
|
|
119
|
+
# これで大半の色コードは出なくなるが、残った ANSI は表示側でも除去する (二重の対策)。
|
|
120
|
+
NO_COLOR_ENV = { "NO_COLOR" => "1", "TERM" => "dumb", "CLICOLOR" => "0" }.freeze
|
|
121
|
+
|
|
122
|
+
def run(script)
|
|
123
|
+
argv = @spec[:argv].call(script)
|
|
124
|
+
out = +""
|
|
125
|
+
err = +""
|
|
126
|
+
status = nil
|
|
127
|
+
|
|
128
|
+
Open3.popen3(NO_COLOR_ENV, @spec[:exe], *argv, chdir: @cwd) do |stdin, stdout, stderr, wait|
|
|
129
|
+
stdin.close
|
|
130
|
+
Timeout.timeout(@timeout_sec) do
|
|
131
|
+
out << stdout.read.to_s
|
|
132
|
+
err << stderr.read.to_s
|
|
133
|
+
status = wait.value
|
|
134
|
+
end
|
|
135
|
+
rescue Timeout::Error
|
|
136
|
+
Process.kill("KILL", wait.pid) rescue nil
|
|
137
|
+
return fail(stderr: "timeout after #{@timeout_sec}s")
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
{
|
|
141
|
+
exit_code: status&.exitstatus || 1,
|
|
142
|
+
stdout: sanitize(out),
|
|
143
|
+
# strip_noise (gsub) は妥当な UTF-8 でないと落ちるため、先に sanitize する。
|
|
144
|
+
stderr: strip_noise(sanitize(err))
|
|
145
|
+
}
|
|
146
|
+
rescue Errno::ENOENT
|
|
147
|
+
fail(stderr: "shell not found: #{@spec[:exe]}")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# 外部プロセスの出力を「上限まで切り詰め + 妥当な UTF-8 に正規化」する。
|
|
151
|
+
# popen3 の出力は ASCII-8BIT で、Windows のコマンドは UTF-8 ではなく CP932 で
|
|
152
|
+
# 日本語を出力するため、UTF-8 誤認で化けないよう to_utf8 で CP932 変換を試みる。
|
|
153
|
+
def sanitize(text)
|
|
154
|
+
text = text.byteslice(0, @max_output_bytes) if text.bytesize > @max_output_bytes
|
|
155
|
+
to_utf8(text)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# WSL bash 実行時に毎回出る無害な systemd 警告。実行結果とは無関係だが、
|
|
159
|
+
# stderr に残ると verifier LLM が失敗と誤判定しやすいため取り除く。
|
|
160
|
+
WSL_SYSTEMD_NOISE = /^wsl: Failed to start the systemd user session.*\n?/
|
|
161
|
+
|
|
162
|
+
def strip_noise(text)
|
|
163
|
+
return text unless @spec[:name] == "bash"
|
|
164
|
+
|
|
165
|
+
text.gsub(WSL_SYSTEMD_NOISE, "")
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Chocomint
|
|
6
|
+
module Tools
|
|
7
|
+
# 許可ディレクトリ内にファイルを書き込む (DESIGN §8)。
|
|
8
|
+
class WriteFile < Base
|
|
9
|
+
def initialize(path_guard:, max_file_bytes:)
|
|
10
|
+
@path_guard = path_guard
|
|
11
|
+
@max_file_bytes = max_file_bytes
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def name = "write_file"
|
|
15
|
+
|
|
16
|
+
def description
|
|
17
|
+
"指定パスにテキストを書き込む。path は許可ディレクトリ内であること。"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def schema
|
|
21
|
+
{
|
|
22
|
+
"type" => "object",
|
|
23
|
+
"properties" => {
|
|
24
|
+
"path" => { "type" => "string" },
|
|
25
|
+
"content" => { "type" => "string" }
|
|
26
|
+
},
|
|
27
|
+
"required" => %w[path content],
|
|
28
|
+
"additionalProperties" => false
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def call(args)
|
|
33
|
+
measure do
|
|
34
|
+
content = args["content"].to_s
|
|
35
|
+
if content.bytesize > @max_file_bytes
|
|
36
|
+
next fail(stderr: "content exceeds max_file_bytes (#{@max_file_bytes})")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
abs = @path_guard.resolve(args["path"])
|
|
40
|
+
FileUtils_mkdir(File.dirname(abs))
|
|
41
|
+
bytes = File.write(abs, content)
|
|
42
|
+
ok(stdout: "wrote #{bytes} bytes to #{args['path']}")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def FileUtils_mkdir(dir)
|
|
49
|
+
require "fileutils"
|
|
50
|
+
FileUtils.mkdir_p(dir)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Chocomint
|
|
4
|
+
module Validator
|
|
5
|
+
# ツール実行結果の機械的検証 (DESIGN §3)。
|
|
6
|
+
# LLM を呼ばずに Ruby だけで判定する。true=合格 / false=RETRY。
|
|
7
|
+
class MachineValidator
|
|
8
|
+
def initialize(path_guard:, max_output_bytes:)
|
|
9
|
+
@path_guard = path_guard
|
|
10
|
+
@max_output_bytes = max_output_bytes
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# proposal: { "tool" => name, "arguments" => {...} }
|
|
14
|
+
# result: { exit_code:, stdout:, stderr:, duration_ms: }
|
|
15
|
+
def valid?(proposal, result)
|
|
16
|
+
return false unless common_checks(result)
|
|
17
|
+
|
|
18
|
+
args = proposal["arguments"]
|
|
19
|
+
case proposal["tool"]
|
|
20
|
+
when "write_file" then write_file_checks(args)
|
|
21
|
+
when "read_file" then result[:stdout].to_s.bytesize >= 0
|
|
22
|
+
when "list_dir" then true
|
|
23
|
+
when "append_file" then File.exist?(@path_guard.resolve(args["path"]))
|
|
24
|
+
when "delete_file" then !File.exist?(@path_guard.resolve(args["path"]))
|
|
25
|
+
when "make_dir" then File.directory?(@path_guard.resolve(args["path"]))
|
|
26
|
+
when "file_info" then json?(result[:stdout])
|
|
27
|
+
when "run_command" then true # exit_code は common_checks で 0 を確認済み
|
|
28
|
+
when "edit" then File.exist?(@path_guard.resolve(args["path"]))
|
|
29
|
+
when "ls" then json?(result[:stdout])
|
|
30
|
+
when "grep", "glob" then true
|
|
31
|
+
when "bash", "powershell" then true # exit_code は common_checks で確認済み
|
|
32
|
+
else true
|
|
33
|
+
end
|
|
34
|
+
rescue StandardError
|
|
35
|
+
false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def common_checks(result)
|
|
41
|
+
result[:exit_code] == 0 &&
|
|
42
|
+
result[:stdout].to_s.bytesize <= @max_output_bytes &&
|
|
43
|
+
result[:stderr].to_s.bytesize <= @max_output_bytes
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# ファイル存在 + 内容一致 (DESIGN §3 File Tool Checks)。
|
|
47
|
+
def write_file_checks(args)
|
|
48
|
+
abs = @path_guard.resolve(args["path"])
|
|
49
|
+
return false unless File.exist?(abs)
|
|
50
|
+
return false unless File.size(abs) >= 0
|
|
51
|
+
|
|
52
|
+
File.read(abs) == args["content"].to_s
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# file_info の stdout が JSON としてパース可能か (DESIGN §3 JSON Tool Checks)。
|
|
56
|
+
def json?(text)
|
|
57
|
+
require "json"
|
|
58
|
+
JSON.parse(text.to_s)
|
|
59
|
+
true
|
|
60
|
+
rescue JSON::ParserError
|
|
61
|
+
false
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Chocomint
|
|
4
|
+
module Validator
|
|
5
|
+
# 副 LLM による意味的検証 (DESIGN §4)。
|
|
6
|
+
# verifier 無効時は常に合格。有効時は 1/0 のみを合否とし、
|
|
7
|
+
# 不正出力 (nil) は「不合格 = RETRY」として扱う (DESIGN §14)。
|
|
8
|
+
class SemanticValidator
|
|
9
|
+
# result にファイル内容を含まないツール (write_file 等は「N bytes 書いた」
|
|
10
|
+
# としか返さない) は、verifier が中身を確認できず不当に不合格判定しがちなので、
|
|
11
|
+
# 対象ファイルの中身を読んで result に付加してから渡す。
|
|
12
|
+
FILE_CONTENT_TOOLS = %w[write_file append_file edit].freeze
|
|
13
|
+
|
|
14
|
+
def initialize(verifier_client:, enabled:, path_guard: nil)
|
|
15
|
+
@verifier_client = verifier_client
|
|
16
|
+
@enabled = enabled
|
|
17
|
+
@path_guard = path_guard
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# 戻り値: [pass(boolean), verifier_output(String|nil)]
|
|
21
|
+
def validate(request:, result:, expectations: nil, trace_id: nil, proposal: nil)
|
|
22
|
+
return [true, nil] unless @enabled
|
|
23
|
+
|
|
24
|
+
output = @verifier_client.verify(
|
|
25
|
+
request: request,
|
|
26
|
+
expectations: expectations,
|
|
27
|
+
result: augment_with_file_content(result, proposal),
|
|
28
|
+
trace_id: trace_id
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
[output == "1", output]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def augment_with_file_content(result, proposal)
|
|
37
|
+
return result unless @path_guard && proposal.is_a?(Hash)
|
|
38
|
+
return result unless FILE_CONTENT_TOOLS.include?(proposal["tool"])
|
|
39
|
+
|
|
40
|
+
path = proposal.dig("arguments", "path")
|
|
41
|
+
return result unless path
|
|
42
|
+
|
|
43
|
+
abs = @path_guard.resolve(path)
|
|
44
|
+
return result unless File.file?(abs)
|
|
45
|
+
|
|
46
|
+
result.merge(file_content: File.read(abs))
|
|
47
|
+
rescue StandardError
|
|
48
|
+
result
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
data/lib/chocomint.rb
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Chocomint
|
|
4
|
+
VERSION = "1.0.0"
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
require_relative "chocomint/errors"
|
|
8
|
+
require_relative "chocomint/config"
|
|
9
|
+
require_relative "chocomint/security/path_guard"
|
|
10
|
+
require_relative "chocomint/tools/base"
|
|
11
|
+
require_relative "chocomint/tools/registry"
|
|
12
|
+
require_relative "chocomint/tools/write_file"
|
|
13
|
+
require_relative "chocomint/tools/read_file"
|
|
14
|
+
require_relative "chocomint/tools/list_dir"
|
|
15
|
+
require_relative "chocomint/tools/append_file"
|
|
16
|
+
require_relative "chocomint/tools/delete_file"
|
|
17
|
+
require_relative "chocomint/tools/file_info"
|
|
18
|
+
require_relative "chocomint/tools/make_dir"
|
|
19
|
+
require_relative "chocomint/tools/run_command"
|
|
20
|
+
require_relative "chocomint/tools/edit"
|
|
21
|
+
require_relative "chocomint/tools/grep"
|
|
22
|
+
require_relative "chocomint/tools/glob"
|
|
23
|
+
require_relative "chocomint/tools/ls"
|
|
24
|
+
require_relative "chocomint/tools/shell"
|
|
25
|
+
require_relative "chocomint/tools/fetch_url"
|
|
26
|
+
require_relative "chocomint/validator/machine_validator"
|
|
27
|
+
require_relative "chocomint/validator/semantic_validator"
|
|
28
|
+
require_relative "chocomint/logger/sqlite_logger"
|
|
29
|
+
require_relative "chocomint/llm/base_client"
|
|
30
|
+
require_relative "chocomint/llm/primary_client"
|
|
31
|
+
require_relative "chocomint/llm/verifier_client"
|
|
32
|
+
require_relative "chocomint/llm/chat_client"
|
|
33
|
+
require_relative "chocomint/orchestrator"
|
|
34
|
+
require_relative "chocomint/planner"
|
|
35
|
+
require_relative "chocomint/factory"
|
|
36
|
+
require_relative "chocomint/server"
|
|
37
|
+
|
|
38
|
+
# AI エディタのコンソール (ConPTY + WebSocket) は Windows + ffi + websocket-driver 依存。
|
|
39
|
+
# 未対応環境や gem 未導入で chocomint 全体のロードが壊れないよう、読み込み失敗は
|
|
40
|
+
# 握りつぶす (コンソール機能なしで動作する)。CONSOLE_AVAILABLE で可否を判定できる。
|
|
41
|
+
module Chocomint
|
|
42
|
+
CONSOLE_AVAILABLE =
|
|
43
|
+
begin
|
|
44
|
+
require_relative "chocomint/console/conpty"
|
|
45
|
+
require_relative "chocomint/console/worker_process"
|
|
46
|
+
require_relative "chocomint/console/server"
|
|
47
|
+
true
|
|
48
|
+
rescue LoadError
|
|
49
|
+
false
|
|
50
|
+
end
|
|
51
|
+
end
|