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.
Files changed (158) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +224 -0
  4. data/THIRD_PARTY_LICENSES.md +45 -0
  5. data/bin/chocomint +37 -0
  6. data/bin/chocomint-console-worker +35 -0
  7. data/bin/chocomint-server +43 -0
  8. data/config/config.yml +37 -0
  9. data/lib/chocomint/config.rb +74 -0
  10. data/lib/chocomint/console/conpty.rb +296 -0
  11. data/lib/chocomint/console/server.rb +164 -0
  12. data/lib/chocomint/console/worker.rb +88 -0
  13. data/lib/chocomint/console/worker_process.rb +254 -0
  14. data/lib/chocomint/edit_git_handlers.rb +397 -0
  15. data/lib/chocomint/edit_handlers.rb +696 -0
  16. data/lib/chocomint/edit_html.rb +9 -0
  17. data/lib/chocomint/errors.rb +18 -0
  18. data/lib/chocomint/factory.rb +145 -0
  19. data/lib/chocomint/llm/base_client.rb +113 -0
  20. data/lib/chocomint/llm/chat_client.rb +206 -0
  21. data/lib/chocomint/llm/primary_client.rb +320 -0
  22. data/lib/chocomint/llm/verifier_client.rb +127 -0
  23. data/lib/chocomint/logger/sqlite_logger.rb +245 -0
  24. data/lib/chocomint/orchestrator.rb +175 -0
  25. data/lib/chocomint/planner.rb +241 -0
  26. data/lib/chocomint/security/path_guard.rb +84 -0
  27. data/lib/chocomint/server.rb +863 -0
  28. data/lib/chocomint/tools/append_file.rb +50 -0
  29. data/lib/chocomint/tools/base.rb +88 -0
  30. data/lib/chocomint/tools/delete_file.rb +42 -0
  31. data/lib/chocomint/tools/edit.rb +65 -0
  32. data/lib/chocomint/tools/fetch_url.rb +156 -0
  33. data/lib/chocomint/tools/file_info.rb +54 -0
  34. data/lib/chocomint/tools/glob.rb +58 -0
  35. data/lib/chocomint/tools/grep.rb +78 -0
  36. data/lib/chocomint/tools/list_dir.rb +41 -0
  37. data/lib/chocomint/tools/ls.rb +51 -0
  38. data/lib/chocomint/tools/make_dir.rb +44 -0
  39. data/lib/chocomint/tools/read_file.rb +45 -0
  40. data/lib/chocomint/tools/registry.rb +29 -0
  41. data/lib/chocomint/tools/run_command.rb +96 -0
  42. data/lib/chocomint/tools/shell.rb +169 -0
  43. data/lib/chocomint/tools/write_file.rb +54 -0
  44. data/lib/chocomint/validator/machine_validator.rb +65 -0
  45. data/lib/chocomint/validator/semantic_validator.rb +52 -0
  46. data/lib/chocomint.rb +51 -0
  47. data/public/edit/edit.css +414 -0
  48. data/public/edit/edit.js +2137 -0
  49. data/public/edit/index.html +251 -0
  50. data/public/vendor/monaco/LICENSE.txt +23 -0
  51. data/public/vendor/monaco/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
  52. data/public/vendor/monaco/vs/base/worker/workerMain.js +31 -0
  53. data/public/vendor/monaco/vs/basic-languages/abap/abap.js +10 -0
  54. data/public/vendor/monaco/vs/basic-languages/apex/apex.js +10 -0
  55. data/public/vendor/monaco/vs/basic-languages/azcli/azcli.js +10 -0
  56. data/public/vendor/monaco/vs/basic-languages/bat/bat.js +10 -0
  57. data/public/vendor/monaco/vs/basic-languages/bicep/bicep.js +11 -0
  58. data/public/vendor/monaco/vs/basic-languages/cameligo/cameligo.js +10 -0
  59. data/public/vendor/monaco/vs/basic-languages/clojure/clojure.js +10 -0
  60. data/public/vendor/monaco/vs/basic-languages/coffee/coffee.js +10 -0
  61. data/public/vendor/monaco/vs/basic-languages/cpp/cpp.js +10 -0
  62. data/public/vendor/monaco/vs/basic-languages/csharp/csharp.js +10 -0
  63. data/public/vendor/monaco/vs/basic-languages/csp/csp.js +10 -0
  64. data/public/vendor/monaco/vs/basic-languages/css/css.js +12 -0
  65. data/public/vendor/monaco/vs/basic-languages/cypher/cypher.js +10 -0
  66. data/public/vendor/monaco/vs/basic-languages/dart/dart.js +10 -0
  67. data/public/vendor/monaco/vs/basic-languages/dockerfile/dockerfile.js +10 -0
  68. data/public/vendor/monaco/vs/basic-languages/ecl/ecl.js +10 -0
  69. data/public/vendor/monaco/vs/basic-languages/elixir/elixir.js +10 -0
  70. data/public/vendor/monaco/vs/basic-languages/flow9/flow9.js +10 -0
  71. data/public/vendor/monaco/vs/basic-languages/freemarker2/freemarker2.js +12 -0
  72. data/public/vendor/monaco/vs/basic-languages/fsharp/fsharp.js +10 -0
  73. data/public/vendor/monaco/vs/basic-languages/go/go.js +10 -0
  74. data/public/vendor/monaco/vs/basic-languages/graphql/graphql.js +10 -0
  75. data/public/vendor/monaco/vs/basic-languages/handlebars/handlebars.js +10 -0
  76. data/public/vendor/monaco/vs/basic-languages/hcl/hcl.js +10 -0
  77. data/public/vendor/monaco/vs/basic-languages/html/html.js +10 -0
  78. data/public/vendor/monaco/vs/basic-languages/ini/ini.js +10 -0
  79. data/public/vendor/monaco/vs/basic-languages/java/java.js +10 -0
  80. data/public/vendor/monaco/vs/basic-languages/javascript/javascript.js +10 -0
  81. data/public/vendor/monaco/vs/basic-languages/julia/julia.js +10 -0
  82. data/public/vendor/monaco/vs/basic-languages/kotlin/kotlin.js +10 -0
  83. data/public/vendor/monaco/vs/basic-languages/less/less.js +11 -0
  84. data/public/vendor/monaco/vs/basic-languages/lexon/lexon.js +10 -0
  85. data/public/vendor/monaco/vs/basic-languages/liquid/liquid.js +10 -0
  86. data/public/vendor/monaco/vs/basic-languages/lua/lua.js +10 -0
  87. data/public/vendor/monaco/vs/basic-languages/m3/m3.js +10 -0
  88. data/public/vendor/monaco/vs/basic-languages/markdown/markdown.js +10 -0
  89. data/public/vendor/monaco/vs/basic-languages/mdx/mdx.js +10 -0
  90. data/public/vendor/monaco/vs/basic-languages/mips/mips.js +10 -0
  91. data/public/vendor/monaco/vs/basic-languages/msdax/msdax.js +10 -0
  92. data/public/vendor/monaco/vs/basic-languages/mysql/mysql.js +10 -0
  93. data/public/vendor/monaco/vs/basic-languages/objective-c/objective-c.js +10 -0
  94. data/public/vendor/monaco/vs/basic-languages/pascal/pascal.js +10 -0
  95. data/public/vendor/monaco/vs/basic-languages/pascaligo/pascaligo.js +10 -0
  96. data/public/vendor/monaco/vs/basic-languages/perl/perl.js +10 -0
  97. data/public/vendor/monaco/vs/basic-languages/pgsql/pgsql.js +10 -0
  98. data/public/vendor/monaco/vs/basic-languages/php/php.js +10 -0
  99. data/public/vendor/monaco/vs/basic-languages/pla/pla.js +10 -0
  100. data/public/vendor/monaco/vs/basic-languages/postiats/postiats.js +10 -0
  101. data/public/vendor/monaco/vs/basic-languages/powerquery/powerquery.js +10 -0
  102. data/public/vendor/monaco/vs/basic-languages/powershell/powershell.js +10 -0
  103. data/public/vendor/monaco/vs/basic-languages/protobuf/protobuf.js +11 -0
  104. data/public/vendor/monaco/vs/basic-languages/pug/pug.js +10 -0
  105. data/public/vendor/monaco/vs/basic-languages/python/python.js +10 -0
  106. data/public/vendor/monaco/vs/basic-languages/qsharp/qsharp.js +10 -0
  107. data/public/vendor/monaco/vs/basic-languages/r/r.js +10 -0
  108. data/public/vendor/monaco/vs/basic-languages/razor/razor.js +10 -0
  109. data/public/vendor/monaco/vs/basic-languages/redis/redis.js +10 -0
  110. data/public/vendor/monaco/vs/basic-languages/redshift/redshift.js +10 -0
  111. data/public/vendor/monaco/vs/basic-languages/restructuredtext/restructuredtext.js +10 -0
  112. data/public/vendor/monaco/vs/basic-languages/ruby/ruby.js +10 -0
  113. data/public/vendor/monaco/vs/basic-languages/rust/rust.js +10 -0
  114. data/public/vendor/monaco/vs/basic-languages/sb/sb.js +10 -0
  115. data/public/vendor/monaco/vs/basic-languages/scala/scala.js +10 -0
  116. data/public/vendor/monaco/vs/basic-languages/scheme/scheme.js +10 -0
  117. data/public/vendor/monaco/vs/basic-languages/scss/scss.js +12 -0
  118. data/public/vendor/monaco/vs/basic-languages/shell/shell.js +10 -0
  119. data/public/vendor/monaco/vs/basic-languages/solidity/solidity.js +10 -0
  120. data/public/vendor/monaco/vs/basic-languages/sophia/sophia.js +10 -0
  121. data/public/vendor/monaco/vs/basic-languages/sparql/sparql.js +10 -0
  122. data/public/vendor/monaco/vs/basic-languages/sql/sql.js +10 -0
  123. data/public/vendor/monaco/vs/basic-languages/st/st.js +10 -0
  124. data/public/vendor/monaco/vs/basic-languages/swift/swift.js +13 -0
  125. data/public/vendor/monaco/vs/basic-languages/systemverilog/systemverilog.js +10 -0
  126. data/public/vendor/monaco/vs/basic-languages/tcl/tcl.js +10 -0
  127. data/public/vendor/monaco/vs/basic-languages/twig/twig.js +10 -0
  128. data/public/vendor/monaco/vs/basic-languages/typescript/typescript.js +10 -0
  129. data/public/vendor/monaco/vs/basic-languages/typespec/typespec.js +10 -0
  130. data/public/vendor/monaco/vs/basic-languages/vb/vb.js +10 -0
  131. data/public/vendor/monaco/vs/basic-languages/wgsl/wgsl.js +307 -0
  132. data/public/vendor/monaco/vs/basic-languages/xml/xml.js +10 -0
  133. data/public/vendor/monaco/vs/basic-languages/yaml/yaml.js +10 -0
  134. data/public/vendor/monaco/vs/editor/editor.main.css +8 -0
  135. data/public/vendor/monaco/vs/editor/editor.main.js +798 -0
  136. data/public/vendor/monaco/vs/language/css/cssMode.js +13 -0
  137. data/public/vendor/monaco/vs/language/css/cssWorker.js +77 -0
  138. data/public/vendor/monaco/vs/language/html/htmlMode.js +13 -0
  139. data/public/vendor/monaco/vs/language/html/htmlWorker.js +454 -0
  140. data/public/vendor/monaco/vs/language/json/jsonMode.js +19 -0
  141. data/public/vendor/monaco/vs/language/json/jsonWorker.js +42 -0
  142. data/public/vendor/monaco/vs/language/typescript/tsMode.js +20 -0
  143. data/public/vendor/monaco/vs/language/typescript/tsWorker.js +51328 -0
  144. data/public/vendor/monaco/vs/loader.js +11 -0
  145. data/public/vendor/monaco/vs/nls.messages.de.js +21 -0
  146. data/public/vendor/monaco/vs/nls.messages.es.js +21 -0
  147. data/public/vendor/monaco/vs/nls.messages.fr.js +19 -0
  148. data/public/vendor/monaco/vs/nls.messages.it.js +19 -0
  149. data/public/vendor/monaco/vs/nls.messages.ja.js +21 -0
  150. data/public/vendor/monaco/vs/nls.messages.ko.js +19 -0
  151. data/public/vendor/monaco/vs/nls.messages.ru.js +21 -0
  152. data/public/vendor/monaco/vs/nls.messages.zh-cn.js +21 -0
  153. data/public/vendor/monaco/vs/nls.messages.zh-tw.js +19 -0
  154. data/public/vendor/xterm/LICENSE +25 -0
  155. data/public/vendor/xterm/addon-fit.js +2 -0
  156. data/public/vendor/xterm/xterm.css +218 -0
  157. data/public/vendor/xterm/xterm.js +2 -0
  158. 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