@satelliteoflove/godot-mcp 3.14.0 → 3.15.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.
- package/addon/command_router.gd +1 -0
- package/addon/commands/exec_commands.gd +117 -0
- package/addon/commands/exec_commands.gd.uid +1 -0
- package/addon/core/mcp_debugger_plugin.gd +24 -0
- package/addon/game_bridge/mcp_exec_guard.gd +225 -0
- package/addon/game_bridge/mcp_exec_guard.gd.uid +1 -0
- package/addon/game_bridge/mcp_game_bridge.gd +233 -1
- package/addon/plugin.cfg +1 -1
- package/dist/__tests__/connection/timeouts.test.js +4 -1
- package/dist/__tests__/connection/timeouts.test.js.map +1 -1
- package/dist/__tests__/tools/exec.test.d.ts +2 -0
- package/dist/__tests__/tools/exec.test.d.ts.map +1 -0
- package/dist/__tests__/tools/exec.test.js +124 -0
- package/dist/__tests__/tools/exec.test.js.map +1 -0
- package/dist/connection/timeouts.d.ts +1 -0
- package/dist/connection/timeouts.d.ts.map +1 -1
- package/dist/connection/timeouts.js +1 -0
- package/dist/connection/timeouts.js.map +1 -1
- package/dist/tools/exec.d.ts +16 -0
- package/dist/tools/exec.d.ts.map +1 -0
- package/dist/tools/exec.js +102 -0
- package/dist/tools/exec.js.map +1 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3 -0
- package/dist/tools/index.js.map +1 -1
- package/package.json +1 -1
package/addon/command_router.gd
CHANGED
|
@@ -23,6 +23,7 @@ func setup(plugin: EditorPlugin) -> void:
|
|
|
23
23
|
_register_handler(MCPProfilerCommands.new(), plugin)
|
|
24
24
|
_register_handler(MCPRuntimeStateCommands.new(), plugin)
|
|
25
25
|
_register_handler(MCPGameTimeCommands.new(), plugin)
|
|
26
|
+
_register_handler(MCPExecCommands.new(), plugin)
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
func _register_handler(handler: MCPBaseCommand, plugin: EditorPlugin) -> void:
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
extends MCPBaseCommand
|
|
3
|
+
class_name MCPExecCommands
|
|
4
|
+
|
|
5
|
+
# godot_exec relay (#243): run / list / remove / clear execute in the game
|
|
6
|
+
# bridge (mcp_game_bridge.gd); this side only forwards over the debugger
|
|
7
|
+
# channel and waits, exactly like game_time_commands.gd. The server derives the
|
|
8
|
+
# timeout cascade from the call's declared budget and pushes relay_timeout_ms
|
|
9
|
+
# in params; the constants are fallbacks for an older server that pushes none.
|
|
10
|
+
# RUN_TIMEOUT is generous because a synchronous user script cannot be aborted
|
|
11
|
+
# mid-flight — the relay waiting is what turns a hung script into a typed
|
|
12
|
+
# TIMEOUT instead of a socket kill.
|
|
13
|
+
const BASE_TIMEOUT := 10.0
|
|
14
|
+
const RUN_TIMEOUT := 28.0
|
|
15
|
+
|
|
16
|
+
var _last_error: Dictionary = {}
|
|
17
|
+
var _call_seq := 0
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
func get_commands() -> Dictionary:
|
|
21
|
+
return {
|
|
22
|
+
"exec_run": exec_run,
|
|
23
|
+
"exec_list": exec_list,
|
|
24
|
+
"exec_remove": exec_remove,
|
|
25
|
+
"exec_clear": exec_clear,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
func exec_run(params: Dictionary) -> Dictionary:
|
|
30
|
+
return await _relay("exec_run", params, _relay_timeout(params, RUN_TIMEOUT))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
func exec_list(params: Dictionary) -> Dictionary:
|
|
34
|
+
return await _relay("exec_list", params, BASE_TIMEOUT)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
func exec_remove(params: Dictionary) -> Dictionary:
|
|
38
|
+
return await _relay("exec_remove", params, BASE_TIMEOUT)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
func exec_clear(params: Dictionary) -> Dictionary:
|
|
42
|
+
return await _relay("exec_clear", params, BASE_TIMEOUT)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
func _relay_timeout(params: Dictionary, fallback: float) -> float:
|
|
46
|
+
# Use the server-pushed relay budget when present (#276); the local constant
|
|
47
|
+
# is only a fallback for an older server that does not derive the cascade.
|
|
48
|
+
var ms: float = float(params.get("relay_timeout_ms", fallback * 1000.0))
|
|
49
|
+
return ms / 1000.0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
func _relay(msg_type: String, params: Dictionary, timeout: float) -> Dictionary:
|
|
53
|
+
# Explicit request/response correlation: the debugger plugin keys responses
|
|
54
|
+
# by msg_type alone, so a timed-out call's LATE response (a slow script that
|
|
55
|
+
# finished after we gave up) could otherwise be consumed as the answer to
|
|
56
|
+
# the next call of the same type — wrong result, silently, and every result
|
|
57
|
+
# after it shifted by one. The bridge echoes call_id; the wait loop discards
|
|
58
|
+
# mismatches. A response with no call_id (older addon) is accepted as-is.
|
|
59
|
+
_call_seq += 1
|
|
60
|
+
var call_id := _call_seq
|
|
61
|
+
params = params.duplicate()
|
|
62
|
+
params["call_id"] = call_id
|
|
63
|
+
var response = await _send_and_wait(msg_type, [params], timeout, call_id)
|
|
64
|
+
if response == null:
|
|
65
|
+
return _last_error
|
|
66
|
+
if response is Dictionary:
|
|
67
|
+
response.erase("call_id") # transport detail, not part of the result
|
|
68
|
+
if response.has("error"):
|
|
69
|
+
return _error("EXEC_ERROR", str(response["error"]))
|
|
70
|
+
return _success(response)
|
|
71
|
+
return _success({"data": response})
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
func _send_and_wait(msg_type: String, args: Array, timeout: float, call_id: int):
|
|
75
|
+
if not EditorInterface.is_playing_scene():
|
|
76
|
+
_last_error = _error("NOT_RUNNING", "No game is currently running")
|
|
77
|
+
return null
|
|
78
|
+
|
|
79
|
+
var debugger_plugin = _plugin.get_debugger_plugin() if _plugin else null
|
|
80
|
+
if debugger_plugin == null or not debugger_plugin.has_active_session():
|
|
81
|
+
_last_error = _error("NO_SESSION", "No active debug session")
|
|
82
|
+
return null
|
|
83
|
+
|
|
84
|
+
var sent: bool = debugger_plugin.send_game_message(msg_type, args)
|
|
85
|
+
if not sent:
|
|
86
|
+
_last_error = _error("SEND_FAILED", "Failed to send message to game")
|
|
87
|
+
return null
|
|
88
|
+
|
|
89
|
+
var start_time := Time.get_ticks_msec()
|
|
90
|
+
while true:
|
|
91
|
+
await Engine.get_main_loop().process_frame
|
|
92
|
+
# A hard runtime error (or failed assert) in exec'd code breaks the game
|
|
93
|
+
# into the editor debugger, suspending the bridge handler mid-call —
|
|
94
|
+
# left alone, the game sits paused and this relay times out.
|
|
95
|
+
# Auto-continue: the handler resumes, the error lands in its logger
|
|
96
|
+
# window, and the response arrives with runtime_errors as designed.
|
|
97
|
+
# Honest limit: the break REASON is not queryable, so this resumes ANY
|
|
98
|
+
# break in the window — including a developer's own breakpoint hit by
|
|
99
|
+
# unrelated game code while an exec call is in flight. Accepted for the
|
|
100
|
+
# dev-tooling threat model and surfaced in the tool description.
|
|
101
|
+
if debugger_plugin.is_session_breaked():
|
|
102
|
+
debugger_plugin.continue_session()
|
|
103
|
+
if debugger_plugin.has_response(msg_type):
|
|
104
|
+
var response = debugger_plugin.get_response(msg_type)
|
|
105
|
+
debugger_plugin.clear_response(msg_type)
|
|
106
|
+
if response is Dictionary and response.has("call_id") \
|
|
107
|
+
and int(response["call_id"]) != call_id:
|
|
108
|
+
continue # a previous call's late response — discard, keep waiting for ours
|
|
109
|
+
return response
|
|
110
|
+
if (Time.get_ticks_msec() - start_time) / 1000.0 > timeout:
|
|
111
|
+
debugger_plugin.clear_response(msg_type)
|
|
112
|
+
var hint := ""
|
|
113
|
+
if debugger_plugin.is_session_breaked():
|
|
114
|
+
hint = " (the game is paused in the editor debugger and did not resume; press Continue in the editor or run godot_editor stop)"
|
|
115
|
+
_last_error = _error("TIMEOUT", "Timed out waiting for %s response%s" % [msg_type, hint])
|
|
116
|
+
return null
|
|
117
|
+
return null # unreachable; satisfies the parser
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://clbw4kvqpf2py
|
|
@@ -326,3 +326,27 @@ func toggle_frame_profiler(enable: bool) -> void:
|
|
|
326
326
|
var session := get_session(_active_session_id)
|
|
327
327
|
if session:
|
|
328
328
|
session.toggle_profiler("mcp_frame_profiler", enable)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
# True when the debugger has paused the running game (script error, failed
|
|
332
|
+
# assert, or breakpoint). A break suspends whatever bridge handler was running
|
|
333
|
+
# mid-call, so relays that would otherwise time out can detect and recover.
|
|
334
|
+
func is_session_breaked() -> bool:
|
|
335
|
+
if _active_session_id < 0:
|
|
336
|
+
return false
|
|
337
|
+
var session := get_session(_active_session_id)
|
|
338
|
+
return session != null and session.is_breaked()
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
# Resume a debugger-paused game. EditorDebuggerSession exposes no continue API,
|
|
342
|
+
# but the raw "continue" command is the same wire message the editor's Continue
|
|
343
|
+
# button sends (ScriptEditorDebugger::_put_msg), and the game's RemoteDebugger
|
|
344
|
+
# handles it in its break loop. Returns false when there is nothing to resume.
|
|
345
|
+
func continue_session() -> bool:
|
|
346
|
+
if _active_session_id < 0:
|
|
347
|
+
return false
|
|
348
|
+
var session := get_session(_active_session_id)
|
|
349
|
+
if session == null or not session.is_breaked():
|
|
350
|
+
return false
|
|
351
|
+
session.send_message("continue", [])
|
|
352
|
+
return true
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
extends RefCounted
|
|
2
|
+
class_name MCPExecGuard
|
|
3
|
+
|
|
4
|
+
## Pure helpers behind godot_exec (#243): the static denylist scan and the
|
|
5
|
+
## function-body wrapper that turn agent-provided GDScript into something the
|
|
6
|
+
## game bridge can compile and run. No engine-state access — everything here is
|
|
7
|
+
## headless-testable (test/exec_guard_headless_test.gd).
|
|
8
|
+
##
|
|
9
|
+
## The scan is an ACCIDENT GUARD, not a security boundary: GDScript cannot be
|
|
10
|
+
## sandboxed, and a determined author can trivially evade a token scan (string-
|
|
11
|
+
## built calls, indirection). Its job is to stop a well-meaning agent from
|
|
12
|
+
## casually reaching outside the game process (spawning processes, writing
|
|
13
|
+
## files, persisting project damage) — the threat model of local dev tooling
|
|
14
|
+
## where the agent already has shell access anyway.
|
|
15
|
+
|
|
16
|
+
# Tokens that reach outside the game process or persist damage to disk.
|
|
17
|
+
# Granularity rationale:
|
|
18
|
+
# - OS.* spawn/kill/shell methods individually (the rest of OS is harmless and
|
|
19
|
+
# frequently legitimate: get_ticks, environment reads, ...).
|
|
20
|
+
# - DirAccess whole-class: every directory mutation lives there; losing
|
|
21
|
+
# read-only listing is acceptable collateral.
|
|
22
|
+
# - FileAccess at the WRITE-constant level: any direct write-mode open must
|
|
23
|
+
# name one of these constants, while read access (loading a save file to
|
|
24
|
+
# inspect it) stays available — which is why FileAccess is not class-blocked.
|
|
25
|
+
# - ResourceSaver whole-class: writes res:// and user:// resources.
|
|
26
|
+
# - EditorInterface: belt-and-suspenders; not normally reachable from a game.
|
|
27
|
+
const DENYLIST: Array[String] = [
|
|
28
|
+
"OS.execute",
|
|
29
|
+
"OS.execute_with_pipe",
|
|
30
|
+
"OS.create_process",
|
|
31
|
+
"OS.create_instance",
|
|
32
|
+
"OS.kill",
|
|
33
|
+
"OS.shell_open",
|
|
34
|
+
"OS.shell_show_in_file_manager",
|
|
35
|
+
"OS.move_to_trash",
|
|
36
|
+
"DirAccess",
|
|
37
|
+
"FileAccess.WRITE",
|
|
38
|
+
"FileAccess.READ_WRITE",
|
|
39
|
+
"FileAccess.WRITE_READ",
|
|
40
|
+
"ResourceSaver",
|
|
41
|
+
"ProjectSettings.save",
|
|
42
|
+
"ProjectSettings.save_custom",
|
|
43
|
+
"EditorInterface",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
# Wrapped user code must compile under projects that escalate warnings to
|
|
47
|
+
# errors, and snippets legitimately ignore parameters, shadow autoload names,
|
|
48
|
+
# discard return values, etc. An invalid warning name here is itself a parse
|
|
49
|
+
# error — pinned by the headless compile test on the Godot version in use.
|
|
50
|
+
const WRAPPER_WARNING_IGNORES := "@warning_ignore(\"unused_parameter\", \"shadowed_global_identifier\", \"shadowed_variable\", \"shadowed_variable_base_class\", \"standalone_expression\", \"return_value_discarded\", \"unused_variable\", \"unreachable_code\", \"integer_division\")"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
## Scan agent-provided source BEFORE compiling. Comments and string contents
|
|
54
|
+
## are stripped first, so `print("OS.execute")` passes while `OS.execute(...)`
|
|
55
|
+
## is caught. Returns {ok: true} or {ok: false, token, message}.
|
|
56
|
+
static func scan_source(source: String) -> Dictionary:
|
|
57
|
+
var lex := _lex(_normalize(source))
|
|
58
|
+
var stripped: String = lex["stripped"]
|
|
59
|
+
if stripped.strip_edges().is_empty():
|
|
60
|
+
return {
|
|
61
|
+
"ok": false,
|
|
62
|
+
"token": "",
|
|
63
|
+
"message": "NO_CODE: exec source contains no executable code (comments only or empty)",
|
|
64
|
+
}
|
|
65
|
+
# Normalize the SCAN copy (never the wrapper input) so the two formatter-
|
|
66
|
+
# plausible token splits — `OS . execute` and a line continuation after the
|
|
67
|
+
# dot — still match. Over-matching here only over-blocks, which is the safe
|
|
68
|
+
# direction for an accident guard.
|
|
69
|
+
stripped = stripped.replace("\\\n", "")
|
|
70
|
+
var dot_ws := RegEx.new()
|
|
71
|
+
dot_ws.compile("\\s*\\.\\s*")
|
|
72
|
+
stripped = dot_ws.sub(stripped, ".", true)
|
|
73
|
+
if _token_re("await").search(stripped) != null:
|
|
74
|
+
return {
|
|
75
|
+
"ok": false,
|
|
76
|
+
"token": "await",
|
|
77
|
+
"message": "SYNC_ONLY: exec source is synchronous-only; 'await' is not allowed. " +
|
|
78
|
+
"For waiting on game state, compose with godot_game_time step/step_until; " +
|
|
79
|
+
"for sustained behavior, attach a node under `holder`.",
|
|
80
|
+
}
|
|
81
|
+
for token in DENYLIST:
|
|
82
|
+
if _token_re(token).search(stripped) != null:
|
|
83
|
+
return {
|
|
84
|
+
"ok": false,
|
|
85
|
+
"token": token,
|
|
86
|
+
"message": "DENIED_TOKEN: source contains '%s' — blocked by the exec denylist " % token +
|
|
87
|
+
"(an accident guard against process/file-write escape, not a security boundary). " +
|
|
88
|
+
"Exec is for mutating the running game's state, not the system around it.",
|
|
89
|
+
}
|
|
90
|
+
return {"ok": true}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
## Wrap user source as the body of `_mcp_run(<binding_names>)` on a RefCounted,
|
|
94
|
+
## so bindings are bare names (`G.wave = 5` just works) and multi-statement
|
|
95
|
+
## code with control flow compiles. Lines inside multiline strings are left
|
|
96
|
+
## untouched (prefixing them would corrupt string content); everything else is
|
|
97
|
+
## prefixed with one indent unit matched to the user's own indent character, so
|
|
98
|
+
## the wrapper can never introduce a mixed-tabs-and-spaces parse error.
|
|
99
|
+
static func build_wrapper(source: String, binding_names: PackedStringArray) -> String:
|
|
100
|
+
var normalized := _normalize(source)
|
|
101
|
+
var lex := _lex(normalized)
|
|
102
|
+
var in_string_lines: Array = lex["in_string_lines"]
|
|
103
|
+
var lines := normalized.split("\n")
|
|
104
|
+
var indent := _detect_indent_unit(lines, in_string_lines, lex["in_bracket_lines"])
|
|
105
|
+
var body: Array = []
|
|
106
|
+
for i in lines.size():
|
|
107
|
+
var line: String = lines[i]
|
|
108
|
+
var starts_in_string: bool = in_string_lines[i] if i < in_string_lines.size() else false
|
|
109
|
+
if starts_in_string or line.strip_edges().is_empty():
|
|
110
|
+
body.append(line)
|
|
111
|
+
else:
|
|
112
|
+
body.append(indent + line)
|
|
113
|
+
return "extends RefCounted\n\n%s\nfunc _mcp_run(%s):\n%s\n" % [
|
|
114
|
+
WRAPPER_WARNING_IGNORES,
|
|
115
|
+
", ".join(binding_names),
|
|
116
|
+
"\n".join(body),
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
static func _normalize(source: String) -> String:
|
|
121
|
+
return source.replace("\r\n", "\n").replace("\r", "\n")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
static func _token_re(token: String) -> RegEx:
|
|
125
|
+
# Word-boundary match so `OS.execute` does not hit `MyOS.executed` — and,
|
|
126
|
+
# because `_` is a word character, `OS.execute` does NOT match
|
|
127
|
+
# `OS.execute_with_pipe` (which has its own entry).
|
|
128
|
+
var re := RegEx.new()
|
|
129
|
+
re.compile("\\b%s\\b" % token.replace(".", "\\."))
|
|
130
|
+
return re
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
## One lexical pass tracking string/comment/bracket state. Returns:
|
|
134
|
+
## stripped: source with comments removed and string CONTENTS removed
|
|
135
|
+
## (quotes kept), line structure preserved — the scan target
|
|
136
|
+
## in_string_lines: per line, whether it STARTS inside a (triple-quoted)
|
|
137
|
+
## string — those lines must not be indent-prefixed
|
|
138
|
+
## in_bracket_lines: per line, whether it STARTS inside an open ()/[]/{} —
|
|
139
|
+
## bracket interiors have free-form indentation, so these
|
|
140
|
+
## lines must not drive indent-unit detection
|
|
141
|
+
## Known accepted miss: raw strings (r"...") treat backslash as an escape here
|
|
142
|
+
## though GDScript does not — the failure mode is over-blocking or an honest
|
|
143
|
+
## compile error, never silent corruption.
|
|
144
|
+
static func _lex(source: String) -> Dictionary:
|
|
145
|
+
var stripped := ""
|
|
146
|
+
var in_string_lines: Array = [false]
|
|
147
|
+
var in_bracket_lines: Array = [false]
|
|
148
|
+
var quote := "" # active quote: '"', "'", '"""', or "'''"
|
|
149
|
+
var depth := 0 # open-bracket depth, tracked outside strings/comments
|
|
150
|
+
var i := 0
|
|
151
|
+
var n := source.length()
|
|
152
|
+
while i < n:
|
|
153
|
+
var c := source[i]
|
|
154
|
+
if quote != "":
|
|
155
|
+
if c == "\n":
|
|
156
|
+
stripped += "\n"
|
|
157
|
+
in_string_lines.append(true)
|
|
158
|
+
in_bracket_lines.append(depth > 0)
|
|
159
|
+
i += 1
|
|
160
|
+
elif c == "\\":
|
|
161
|
+
# An escaped char never ends the string — but an escaped
|
|
162
|
+
# NEWLINE (a line continuation inside the string) still starts
|
|
163
|
+
# a new physical line. Record it, or every following line would
|
|
164
|
+
# be mis-indexed and build_wrapper would prefix indent into the
|
|
165
|
+
# string's runtime value: silent corruption.
|
|
166
|
+
if i + 1 < n and source[i + 1] == "\n":
|
|
167
|
+
stripped += "\n"
|
|
168
|
+
in_string_lines.append(true)
|
|
169
|
+
in_bracket_lines.append(depth > 0)
|
|
170
|
+
i += 2
|
|
171
|
+
elif source.substr(i, quote.length()) == quote:
|
|
172
|
+
stripped += quote
|
|
173
|
+
i += quote.length()
|
|
174
|
+
quote = ""
|
|
175
|
+
else:
|
|
176
|
+
i += 1
|
|
177
|
+
continue
|
|
178
|
+
if c == "#":
|
|
179
|
+
while i < n and source[i] != "\n":
|
|
180
|
+
i += 1
|
|
181
|
+
continue
|
|
182
|
+
if c == "\"" or c == "'":
|
|
183
|
+
quote = c.repeat(3) if source.substr(i, 3) == c.repeat(3) else c
|
|
184
|
+
stripped += quote
|
|
185
|
+
i += quote.length()
|
|
186
|
+
continue
|
|
187
|
+
if c == "(" or c == "[" or c == "{":
|
|
188
|
+
depth += 1
|
|
189
|
+
elif c == ")" or c == "]" or c == "}":
|
|
190
|
+
depth = maxi(0, depth - 1)
|
|
191
|
+
if c == "\n":
|
|
192
|
+
stripped += "\n"
|
|
193
|
+
in_string_lines.append(false)
|
|
194
|
+
in_bracket_lines.append(depth > 0)
|
|
195
|
+
i += 1
|
|
196
|
+
continue
|
|
197
|
+
stripped += c
|
|
198
|
+
i += 1
|
|
199
|
+
return {
|
|
200
|
+
"stripped": stripped,
|
|
201
|
+
"in_string_lines": in_string_lines,
|
|
202
|
+
"in_bracket_lines": in_bracket_lines,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
static func _detect_indent_unit(lines: PackedStringArray, in_string_lines: Array, in_bracket_lines: Array) -> String:
|
|
207
|
+
# Match the user's indent character: GDScript rejects mixed tabs/spaces in
|
|
208
|
+
# one indentation run, so the wrapper prefix must agree with the body. Four
|
|
209
|
+
# spaces composes with any space width (nesting only has to increase).
|
|
210
|
+
# Bracket-continuation lines are skipped: their indentation is free-form
|
|
211
|
+
# (a space-aligned array literal in otherwise tab-indented code is legal),
|
|
212
|
+
# so they must not decide the prefix for real block lines.
|
|
213
|
+
for i in lines.size():
|
|
214
|
+
if i < in_string_lines.size() and in_string_lines[i]:
|
|
215
|
+
continue
|
|
216
|
+
if i < in_bracket_lines.size() and in_bracket_lines[i]:
|
|
217
|
+
continue
|
|
218
|
+
var line := lines[i]
|
|
219
|
+
if line.strip_edges().is_empty():
|
|
220
|
+
continue
|
|
221
|
+
if line.begins_with("\t"):
|
|
222
|
+
return "\t"
|
|
223
|
+
if line.begins_with(" "):
|
|
224
|
+
return " "
|
|
225
|
+
return "\t"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://cdscqjpsiv412
|
|
@@ -374,6 +374,18 @@ func _on_debugger_message(message: String, data: Array) -> bool:
|
|
|
374
374
|
"game_time_status":
|
|
375
375
|
_handle_game_time_status(data)
|
|
376
376
|
return true
|
|
377
|
+
"exec_run":
|
|
378
|
+
_handle_exec_run(data)
|
|
379
|
+
return true
|
|
380
|
+
"exec_list":
|
|
381
|
+
_handle_exec_list(data)
|
|
382
|
+
return true
|
|
383
|
+
"exec_remove":
|
|
384
|
+
_handle_exec_remove(data)
|
|
385
|
+
return true
|
|
386
|
+
"exec_clear":
|
|
387
|
+
_handle_exec_clear(data)
|
|
388
|
+
return true
|
|
377
389
|
return false
|
|
378
390
|
|
|
379
391
|
|
|
@@ -1031,7 +1043,7 @@ func _list_autoload_paths(scene_root: Node) -> Array:
|
|
|
1031
1043
|
if tree == null or tree.root == null:
|
|
1032
1044
|
return out
|
|
1033
1045
|
for child in tree.root.get_children():
|
|
1034
|
-
if child == scene_root or child == self:
|
|
1046
|
+
if child == scene_root or child == self or child == _exec_holder:
|
|
1035
1047
|
continue
|
|
1036
1048
|
out.append("/root/" + str(child.name))
|
|
1037
1049
|
return out
|
|
@@ -1075,6 +1087,10 @@ func _handle_watch_stop() -> void:
|
|
|
1075
1087
|
class _MCPGameLogger extends Logger:
|
|
1076
1088
|
var _output: PackedStringArray = []
|
|
1077
1089
|
var _max_lines := 1000
|
|
1090
|
+
# Lines trimmed off the front of the ring buffer, ever. Lets a caller hold a
|
|
1091
|
+
# STABLE mark (dropped + size) across trims instead of a raw index that
|
|
1092
|
+
# silently drifts when the buffer overflows (exec's runtime-error window).
|
|
1093
|
+
var _dropped := 0
|
|
1078
1094
|
var _mutex := Mutex.new()
|
|
1079
1095
|
|
|
1080
1096
|
func _log_message(message: String, error: bool) -> void:
|
|
@@ -1083,6 +1099,7 @@ class _MCPGameLogger extends Logger:
|
|
|
1083
1099
|
_output.append(prefix + message)
|
|
1084
1100
|
if _output.size() > _max_lines:
|
|
1085
1101
|
_output.remove_at(0)
|
|
1102
|
+
_dropped += 1
|
|
1086
1103
|
_mutex.unlock()
|
|
1087
1104
|
|
|
1088
1105
|
func _log_error(function: String, file: String, line: int, code: String,
|
|
@@ -1093,13 +1110,18 @@ class _MCPGameLogger extends Logger:
|
|
|
1093
1110
|
_output.append("[ERROR] " + msg)
|
|
1094
1111
|
if _output.size() > _max_lines:
|
|
1095
1112
|
_output.remove_at(0)
|
|
1113
|
+
_dropped += 1
|
|
1096
1114
|
_mutex.unlock()
|
|
1097
1115
|
|
|
1098
1116
|
func get_output() -> PackedStringArray:
|
|
1099
1117
|
return _output
|
|
1100
1118
|
|
|
1119
|
+
func get_dropped() -> int:
|
|
1120
|
+
return _dropped
|
|
1121
|
+
|
|
1101
1122
|
func clear() -> void:
|
|
1102
1123
|
_mutex.lock()
|
|
1124
|
+
_dropped += _output.size() # cleared lines are gone the same as trimmed ones
|
|
1103
1125
|
_output.clear()
|
|
1104
1126
|
_mutex.unlock()
|
|
1105
1127
|
|
|
@@ -1844,3 +1866,213 @@ func _finish_step() -> void:
|
|
|
1844
1866
|
_step_predicate_inputs = []
|
|
1845
1867
|
_step_report = []
|
|
1846
1868
|
_send_game_time_response(response_type, result)
|
|
1869
|
+
|
|
1870
|
+
|
|
1871
|
+
# ── godot_exec: run agent-provided GDScript in this game process (#243) ───────
|
|
1872
|
+
#
|
|
1873
|
+
# One-shot scripts compile as the body of `_mcp_run(<bindings>)` (see
|
|
1874
|
+
# MCPExecGuard.build_wrapper) and run synchronously right here. Persistent
|
|
1875
|
+
# behaviors are nodes the script attaches under the `holder` binding — a child
|
|
1876
|
+
# of /root, NOT of this bridge: bridge children inherit PROCESS_MODE_ALWAYS and
|
|
1877
|
+
# would keep acting under a freeze, while a root child pauses with the tree
|
|
1878
|
+
# (bots respect freeze/step) yet survives scene reloads. The game process dying
|
|
1879
|
+
# on stop is the cleanup guarantee.
|
|
1880
|
+
|
|
1881
|
+
# Cap on runtime-error lines echoed back per exec — the full text stays in the
|
|
1882
|
+
# game console either way.
|
|
1883
|
+
const EXEC_MAX_ERROR_LINES := 20
|
|
1884
|
+
|
|
1885
|
+
var _exec_holder: Node = null
|
|
1886
|
+
|
|
1887
|
+
|
|
1888
|
+
func _exec_params(data: Array) -> Dictionary:
|
|
1889
|
+
return data[0] if data.size() > 0 and data[0] is Dictionary else {}
|
|
1890
|
+
|
|
1891
|
+
|
|
1892
|
+
# Responses correlate by message type alone in the editor plugin, so a late
|
|
1893
|
+
# response from a timed-out call could be consumed as the answer to the NEXT
|
|
1894
|
+
# call of the same type — wrong result, silently. Echoing the relay's call_id
|
|
1895
|
+
# lets the relay discard mismatches. Absent when the relay pushed none (older
|
|
1896
|
+
# server): the relay accepts unmatched responses then, so skew is safe both ways.
|
|
1897
|
+
func _send_exec_response(msg_type: String, result: Dictionary, params: Dictionary) -> void:
|
|
1898
|
+
if params.has("call_id"):
|
|
1899
|
+
result["call_id"] = params["call_id"]
|
|
1900
|
+
EngineDebugger.send_message("godot_mcp:game_response", [msg_type, result])
|
|
1901
|
+
|
|
1902
|
+
|
|
1903
|
+
func _ensure_exec_holder() -> Node:
|
|
1904
|
+
if _exec_holder != null and is_instance_valid(_exec_holder):
|
|
1905
|
+
return _exec_holder
|
|
1906
|
+
_exec_holder = Node.new()
|
|
1907
|
+
_exec_holder.name = "MCPExecHolder"
|
|
1908
|
+
# Stamp attach time on whatever user scripts add, so exec_list can report an
|
|
1909
|
+
# age without trusting the script to record one.
|
|
1910
|
+
_exec_holder.child_entered_tree.connect(func(child: Node) -> void:
|
|
1911
|
+
child.set_meta("mcp_exec_attached_ms", Time.get_ticks_msec()))
|
|
1912
|
+
get_tree().root.add_child(_exec_holder)
|
|
1913
|
+
return _exec_holder
|
|
1914
|
+
|
|
1915
|
+
|
|
1916
|
+
# A stable position in the logger stream (survives ring-buffer trims): lines
|
|
1917
|
+
# appended ever = dropped + currently held.
|
|
1918
|
+
func _exec_logger_mark() -> int:
|
|
1919
|
+
return (_logger.get_dropped() + _logger.get_output().size()) if _logger else 0
|
|
1920
|
+
|
|
1921
|
+
|
|
1922
|
+
# The window of logger lines produced since `mark` (an _exec_logger_mark
|
|
1923
|
+
# value), error lines only. Process-wide, not per-script: a concurrent game
|
|
1924
|
+
# error inside the window rides along — acceptable for an honest "what went
|
|
1925
|
+
# wrong" echo. If the ring buffer trimmed past the mark (a >1000-line script),
|
|
1926
|
+
# the lost stretch is reported instead of silently misattributed.
|
|
1927
|
+
func _exec_logger_delta(mark: int) -> Array:
|
|
1928
|
+
var out: Array = []
|
|
1929
|
+
if _logger == null:
|
|
1930
|
+
return out
|
|
1931
|
+
var lines := _logger.get_output()
|
|
1932
|
+
var start := mark - _logger.get_dropped()
|
|
1933
|
+
if start < 0:
|
|
1934
|
+
out.append("... (log buffer overflowed; %d earlier lines lost — see the game console)" % -start)
|
|
1935
|
+
start = 0
|
|
1936
|
+
for i in range(start, lines.size()):
|
|
1937
|
+
if not lines[i].begins_with("[ERROR] "):
|
|
1938
|
+
continue
|
|
1939
|
+
if out.size() >= EXEC_MAX_ERROR_LINES:
|
|
1940
|
+
out.append("... (more errors truncated; see the game console)")
|
|
1941
|
+
break
|
|
1942
|
+
out.append(lines[i].substr(8))
|
|
1943
|
+
return out
|
|
1944
|
+
|
|
1945
|
+
|
|
1946
|
+
func _build_exec_context() -> Dictionary:
|
|
1947
|
+
# The step_until predicate context (autoloads by name + tree/root), plus
|
|
1948
|
+
# `holder`. If a project defines an autoload literally named "holder", the
|
|
1949
|
+
# autoload keeps the name (a duplicate parameter would be a parse error);
|
|
1950
|
+
# the exec holder is still reachable as root.get_node("MCPExecHolder").
|
|
1951
|
+
var ctx := _build_predicate_context()
|
|
1952
|
+
var names: PackedStringArray = ctx["names"]
|
|
1953
|
+
var inputs: Array = ctx["inputs"]
|
|
1954
|
+
if not ("holder" in names):
|
|
1955
|
+
names.append("holder")
|
|
1956
|
+
inputs.append(_ensure_exec_holder())
|
|
1957
|
+
return {"names": names, "inputs": inputs}
|
|
1958
|
+
|
|
1959
|
+
|
|
1960
|
+
func _handle_exec_run(data: Array) -> void:
|
|
1961
|
+
var params := _exec_params(data)
|
|
1962
|
+
var source: String = str(params.get("source", ""))
|
|
1963
|
+
|
|
1964
|
+
var scan := MCPExecGuard.scan_source(source)
|
|
1965
|
+
if not scan.get("ok", false):
|
|
1966
|
+
_send_exec_response("exec_run", {"error": str(scan.get("message", "exec source rejected"))}, params)
|
|
1967
|
+
return
|
|
1968
|
+
|
|
1969
|
+
var ctx := _build_exec_context()
|
|
1970
|
+
var script := GDScript.new()
|
|
1971
|
+
script.source_code = MCPExecGuard.build_wrapper(source, ctx["names"])
|
|
1972
|
+
|
|
1973
|
+
var mark := _exec_logger_mark()
|
|
1974
|
+
if script.reload() != OK or not script.can_instantiate():
|
|
1975
|
+
var detail := "; ".join(PackedStringArray(_exec_logger_delta(mark)))
|
|
1976
|
+
var msg := "exec compile error"
|
|
1977
|
+
if detail.is_empty():
|
|
1978
|
+
msg += " (parser text not captured; check the game console, e.g. minimal-godot-mcp get_console_output)"
|
|
1979
|
+
else:
|
|
1980
|
+
msg += ": " + detail
|
|
1981
|
+
_send_exec_response("exec_run", {"error": msg}, params)
|
|
1982
|
+
return
|
|
1983
|
+
|
|
1984
|
+
var inst: Object = script.new()
|
|
1985
|
+
mark = _exec_logger_mark()
|
|
1986
|
+
var t0 := Time.get_ticks_msec()
|
|
1987
|
+
# Synchronous and non-preemptible: an infinite loop here hangs the game (the
|
|
1988
|
+
# relay/server time out; godot_editor stop kills the process). That is the
|
|
1989
|
+
# documented contract — no wall budget is pretended.
|
|
1990
|
+
var result: Variant = inst.callv("_mcp_run", ctx["inputs"])
|
|
1991
|
+
var duration := Time.get_ticks_msec() - t0
|
|
1992
|
+
|
|
1993
|
+
# Runtime backstop for the scanner's SYNC_ONLY rule (a string-built await
|
|
1994
|
+
# can slip past a token scan): a suspended call returns a function state.
|
|
1995
|
+
if typeof(result) == TYPE_OBJECT and result != null \
|
|
1996
|
+
and result.get_class() == "GDScriptFunctionState":
|
|
1997
|
+
_send_exec_response("exec_run", {"error":
|
|
1998
|
+
"SCRIPT_SUSPENDED: the script hit an await and suspended (exec is synchronous-only; " +
|
|
1999
|
+
"side effects before the await have already run). Use godot_game_time step/step_until to wait."}, params)
|
|
2000
|
+
return
|
|
2001
|
+
|
|
2002
|
+
var out: Dictionary = {
|
|
2003
|
+
"completed": true,
|
|
2004
|
+
"result": _sanitize_value(result),
|
|
2005
|
+
"duration_ms": duration,
|
|
2006
|
+
"holder_children": _exec_holder.get_child_count() \
|
|
2007
|
+
if _exec_holder != null and is_instance_valid(_exec_holder) else 0,
|
|
2008
|
+
}
|
|
2009
|
+
var errs := _exec_logger_delta(mark)
|
|
2010
|
+
if not errs.is_empty():
|
|
2011
|
+
out["runtime_errors"] = errs
|
|
2012
|
+
_send_exec_response("exec_run", out, params)
|
|
2013
|
+
|
|
2014
|
+
|
|
2015
|
+
func _handle_exec_list(data: Array) -> void:
|
|
2016
|
+
var params := _exec_params(data)
|
|
2017
|
+
var nodes: Array = []
|
|
2018
|
+
if _exec_holder != null and is_instance_valid(_exec_holder):
|
|
2019
|
+
var now := Time.get_ticks_msec()
|
|
2020
|
+
for child in _exec_holder.get_children():
|
|
2021
|
+
var script_chars := 0
|
|
2022
|
+
var s: Variant = child.get_script()
|
|
2023
|
+
if s is GDScript:
|
|
2024
|
+
script_chars = (s as GDScript).source_code.length()
|
|
2025
|
+
nodes.append({
|
|
2026
|
+
"name": str(child.name),
|
|
2027
|
+
"class": child.get_class(),
|
|
2028
|
+
"script_chars": script_chars,
|
|
2029
|
+
"age_ms": now - int(child.get_meta("mcp_exec_attached_ms", now)),
|
|
2030
|
+
# Internal processing too: Timers and tweened nodes drive
|
|
2031
|
+
# themselves internally and would otherwise read as idle.
|
|
2032
|
+
"processing": child.is_processing() or child.is_physics_processing() \
|
|
2033
|
+
or child.is_processing_internal() or child.is_physics_processing_internal(),
|
|
2034
|
+
})
|
|
2035
|
+
_send_exec_response("exec_list", {"nodes": nodes, "count": nodes.size()}, params)
|
|
2036
|
+
|
|
2037
|
+
|
|
2038
|
+
func _handle_exec_remove(data: Array) -> void:
|
|
2039
|
+
var params := _exec_params(data)
|
|
2040
|
+
var node_name := str(params.get("name", ""))
|
|
2041
|
+
var child: Node = null
|
|
2042
|
+
if _exec_holder != null and is_instance_valid(_exec_holder) and not node_name.is_empty():
|
|
2043
|
+
# Name EQUALITY against direct children — never a NodePath lookup: a
|
|
2044
|
+
# path-like name would traverse out of the holder (".." resolves to
|
|
2045
|
+
# /root, "a/b" to a grandchild) and queue_free whatever it lands on.
|
|
2046
|
+
for c in _exec_holder.get_children():
|
|
2047
|
+
if str(c.name) == node_name:
|
|
2048
|
+
child = c
|
|
2049
|
+
break
|
|
2050
|
+
if child == null:
|
|
2051
|
+
var have: Array = []
|
|
2052
|
+
if _exec_holder != null and is_instance_valid(_exec_holder):
|
|
2053
|
+
for c in _exec_holder.get_children():
|
|
2054
|
+
have.append(str(c.name))
|
|
2055
|
+
_send_exec_response("exec_remove", {"error":
|
|
2056
|
+
"NOT_FOUND: no exec node named '%s' (have: %s)" % [
|
|
2057
|
+
node_name, ", ".join(PackedStringArray(have)) if not have.is_empty() else "none"]}, params)
|
|
2058
|
+
return
|
|
2059
|
+
# Detach immediately so a list right after this call already shows it gone;
|
|
2060
|
+
# queue_free still frees the detached node at the end of the frame.
|
|
2061
|
+
_exec_holder.remove_child(child)
|
|
2062
|
+
child.queue_free()
|
|
2063
|
+
_send_exec_response("exec_remove", {
|
|
2064
|
+
"removed": true,
|
|
2065
|
+
"name": node_name,
|
|
2066
|
+
"remaining": _exec_holder.get_child_count(),
|
|
2067
|
+
}, params)
|
|
2068
|
+
|
|
2069
|
+
|
|
2070
|
+
func _handle_exec_clear(data: Array) -> void:
|
|
2071
|
+
var params := _exec_params(data)
|
|
2072
|
+
var removed := 0
|
|
2073
|
+
if _exec_holder != null and is_instance_valid(_exec_holder):
|
|
2074
|
+
for child in _exec_holder.get_children():
|
|
2075
|
+
_exec_holder.remove_child(child)
|
|
2076
|
+
child.queue_free()
|
|
2077
|
+
removed += 1
|
|
2078
|
+
_send_exec_response("exec_clear", {"removed_count": removed}, params)
|
package/addon/plugin.cfg
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { deriveTimeouts, maxInGameBudgetMs, QUICK_TIMEOUT_MS, ABSOLUTE_CEILING_MS, READY_WAIT_MS, BRIDGE_WALL_SLOP_MS, STEP_BUDGET_CAP_MS, INPUT_BUDGET_CAP_MS, } from '../../connection/timeouts.js';
|
|
2
|
+
import { deriveTimeouts, maxInGameBudgetMs, QUICK_TIMEOUT_MS, ABSOLUTE_CEILING_MS, READY_WAIT_MS, BRIDGE_WALL_SLOP_MS, STEP_BUDGET_CAP_MS, INPUT_BUDGET_CAP_MS, EXEC_BUDGET_CAP_MS, } from '../../connection/timeouts.js';
|
|
3
3
|
describe('timeout cascade (#276)', () => {
|
|
4
4
|
describe('deriveTimeouts stagger invariant', () => {
|
|
5
5
|
// The whole point: bridge answers before relay answers before the server
|
|
@@ -50,6 +50,9 @@ describe('timeout cascade (#276)', () => {
|
|
|
50
50
|
it('input cap <= max in-game budget (with ready-wait)', () => {
|
|
51
51
|
expect(INPUT_BUDGET_CAP_MS).toBeLessThanOrEqual(maxInGameBudgetMs({ readyWait: true }));
|
|
52
52
|
});
|
|
53
|
+
it('exec cap <= max in-game budget (no ready-wait)', () => {
|
|
54
|
+
expect(EXEC_BUDGET_CAP_MS).toBeLessThanOrEqual(maxInGameBudgetMs({ readyWait: false }));
|
|
55
|
+
});
|
|
53
56
|
});
|
|
54
57
|
it('keeps the quick default unchanged at 30s', () => {
|
|
55
58
|
expect(QUICK_TIMEOUT_MS).toBe(30_000);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timeouts.test.js","sourceRoot":"","sources":["../../../src/__tests__/connection/timeouts.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,mBAAmB,EACnB,aAAa,EACb,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,
|
|
1
|
+
{"version":3,"file":"timeouts.test.js","sourceRoot":"","sources":["../../../src/__tests__/connection/timeouts.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exec.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/tools/exec.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { createMockGodot, createToolContext, structuredOf } from '../helpers/mock-godot.js';
|
|
3
|
+
import { exec } from '../../tools/exec.js';
|
|
4
|
+
import { deriveTimeouts, EXEC_BUDGET_CAP_MS } from '../../connection/timeouts.js';
|
|
5
|
+
describe('godot_exec tool', () => {
|
|
6
|
+
let mock;
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
mock = createMockGodot();
|
|
9
|
+
});
|
|
10
|
+
describe('schema validation', () => {
|
|
11
|
+
it('run requires a non-empty source within the size cap', () => {
|
|
12
|
+
expect(exec.schema.safeParse({ action: 'run' }).success).toBe(false);
|
|
13
|
+
expect(exec.schema.safeParse({ action: 'run', source: '' }).success).toBe(false);
|
|
14
|
+
expect(exec.schema.safeParse({ action: 'run', source: 'return 1' }).success).toBe(true);
|
|
15
|
+
expect(exec.schema.safeParse({ action: 'run', source: 'x'.repeat(16_384) }).success).toBe(true);
|
|
16
|
+
expect(exec.schema.safeParse({ action: 'run', source: 'x'.repeat(16_385) }).success).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
it('run enforces the published budget cap', () => {
|
|
19
|
+
expect(exec.schema.safeParse({ action: 'run', source: 'pass', budget_ms: EXEC_BUDGET_CAP_MS }).success).toBe(true);
|
|
20
|
+
expect(exec.schema.safeParse({ action: 'run', source: 'pass', budget_ms: EXEC_BUDGET_CAP_MS + 1 }).success).toBe(false);
|
|
21
|
+
expect(exec.schema.safeParse({ action: 'run', source: 'pass', budget_ms: 0 }).success).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
it('remove requires a name; list and clear take no extra arguments', () => {
|
|
24
|
+
expect(exec.schema.safeParse({ action: 'remove' }).success).toBe(false);
|
|
25
|
+
expect(exec.schema.safeParse({ action: 'remove', name: '' }).success).toBe(false);
|
|
26
|
+
expect(exec.schema.safeParse({ action: 'remove', name: 'GodGuard' }).success).toBe(true);
|
|
27
|
+
expect(exec.schema.safeParse({ action: 'list' }).success).toBe(true);
|
|
28
|
+
expect(exec.schema.safeParse({ action: 'clear' }).success).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
describe('run', () => {
|
|
32
|
+
it('forwards the source and returns the structured result', async () => {
|
|
33
|
+
mock.mockResponse({ completed: true, result: 5, duration_ms: 2, holder_children: 0 });
|
|
34
|
+
const ctx = createToolContext(mock);
|
|
35
|
+
const result = await exec.execute({ action: 'run', source: 'G.wave = 5\nreturn G.wave' }, ctx);
|
|
36
|
+
expect(mock.calls[0].command).toBe('exec_run');
|
|
37
|
+
expect(mock.calls[0].params.source).toBe('G.wave = 5\nreturn G.wave');
|
|
38
|
+
const data = structuredOf(result);
|
|
39
|
+
expect(data.completed).toBe(true);
|
|
40
|
+
expect(data.result).toBe(5);
|
|
41
|
+
});
|
|
42
|
+
it('passes runtime_errors through (the call still completes)', async () => {
|
|
43
|
+
mock.mockResponse({
|
|
44
|
+
completed: true,
|
|
45
|
+
result: null,
|
|
46
|
+
duration_ms: 1,
|
|
47
|
+
holder_children: 0,
|
|
48
|
+
runtime_errors: ["Invalid access to property or key 'foo' on a base object of type 'Nil'."],
|
|
49
|
+
});
|
|
50
|
+
const ctx = createToolContext(mock);
|
|
51
|
+
const result = await exec.execute({ action: 'run', source: 'var x = null\nreturn x.foo' }, ctx);
|
|
52
|
+
const data = structuredOf(result);
|
|
53
|
+
expect(data.completed).toBe(true);
|
|
54
|
+
expect(data.runtime_errors).toHaveLength(1);
|
|
55
|
+
});
|
|
56
|
+
it('derives the timeout cascade from budget_ms and pushes the relay budget', async () => {
|
|
57
|
+
mock.mockResponse({ completed: true, result: null, duration_ms: 0, holder_children: 0 });
|
|
58
|
+
const ctx = createToolContext(mock);
|
|
59
|
+
await exec.execute({ action: 'run', source: 'pass', budget_ms: 5000 }, ctx);
|
|
60
|
+
const call = mock.calls[0];
|
|
61
|
+
const t = deriveTimeouts(5000); // no ready-wait, same class as game_time
|
|
62
|
+
expect(call.params.relay_timeout_ms).toBe(t.relayMs);
|
|
63
|
+
expect(call.opts?.timeoutMs).toBe(t.serverMs);
|
|
64
|
+
// No wall budget: a synchronous script cannot be aborted, so none is pretended.
|
|
65
|
+
expect(call.params.wall_budget_ms).toBeUndefined();
|
|
66
|
+
});
|
|
67
|
+
it('defaults the budget to 10s when omitted', async () => {
|
|
68
|
+
mock.mockResponse({ completed: true, result: null, duration_ms: 0, holder_children: 0 });
|
|
69
|
+
const ctx = createToolContext(mock);
|
|
70
|
+
await exec.execute({ action: 'run', source: 'pass' }, ctx);
|
|
71
|
+
expect(mock.calls[0].opts?.timeoutMs).toBe(deriveTimeouts(10_000).serverMs);
|
|
72
|
+
});
|
|
73
|
+
it('propagates a bridge-side rejection (denylist, compile error)', async () => {
|
|
74
|
+
mock.mockError(new Error("DENIED_TOKEN: source contains 'OS.execute'"));
|
|
75
|
+
const ctx = createToolContext(mock);
|
|
76
|
+
await expect(exec.execute({ action: 'run', source: 'OS.execute("cmd", [])' }, ctx)).rejects.toThrow('DENIED_TOKEN');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
describe('lifecycle', () => {
|
|
80
|
+
it('list returns the structured holder inventory', async () => {
|
|
81
|
+
mock.mockResponse({
|
|
82
|
+
nodes: [{ name: 'GodGuard', class: 'Timer', script_chars: 0, age_ms: 4200, processing: true }],
|
|
83
|
+
count: 1,
|
|
84
|
+
});
|
|
85
|
+
const ctx = createToolContext(mock);
|
|
86
|
+
const result = await exec.execute({ action: 'list' }, ctx);
|
|
87
|
+
expect(mock.calls[0].command).toBe('exec_list');
|
|
88
|
+
const data = structuredOf(result);
|
|
89
|
+
expect(data.count).toBe(1);
|
|
90
|
+
expect(data.nodes[0].name).toBe('GodGuard');
|
|
91
|
+
});
|
|
92
|
+
it('list keeps the structured shape when the holder is empty', async () => {
|
|
93
|
+
mock.mockResponse({ nodes: [], count: 0 });
|
|
94
|
+
const ctx = createToolContext(mock);
|
|
95
|
+
const result = await exec.execute({ action: 'list' }, ctx);
|
|
96
|
+
const data = structuredOf(result);
|
|
97
|
+
expect(data.count).toBe(0);
|
|
98
|
+
expect(data.nodes).toEqual([]);
|
|
99
|
+
});
|
|
100
|
+
it('remove forwards the name and returns the structured result', async () => {
|
|
101
|
+
mock.mockResponse({ removed: true, name: 'GodGuard', remaining: 0 });
|
|
102
|
+
const ctx = createToolContext(mock);
|
|
103
|
+
const result = await exec.execute({ action: 'remove', name: 'GodGuard' }, ctx);
|
|
104
|
+
expect(mock.calls[0].command).toBe('exec_remove');
|
|
105
|
+
expect(mock.calls[0].params.name).toBe('GodGuard');
|
|
106
|
+
const data = structuredOf(result);
|
|
107
|
+
expect(data.removed).toBe(true);
|
|
108
|
+
expect(data.remaining).toBe(0);
|
|
109
|
+
});
|
|
110
|
+
it('remove propagates NOT_FOUND from the bridge', async () => {
|
|
111
|
+
mock.mockError(new Error("NOT_FOUND: no exec node named 'Bogus' (have: GodGuard)"));
|
|
112
|
+
const ctx = createToolContext(mock);
|
|
113
|
+
await expect(exec.execute({ action: 'remove', name: 'Bogus' }, ctx)).rejects.toThrow('NOT_FOUND');
|
|
114
|
+
});
|
|
115
|
+
it('clear reports the removed count', async () => {
|
|
116
|
+
mock.mockResponse({ removed_count: 2 });
|
|
117
|
+
const ctx = createToolContext(mock);
|
|
118
|
+
const result = await exec.execute({ action: 'clear' }, ctx);
|
|
119
|
+
expect(mock.calls[0].command).toBe('exec_clear');
|
|
120
|
+
expect(result).toContain('Removed 2');
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
//# sourceMappingURL=exec.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exec.test.js","sourceRoot":"","sources":["../../../src/__tests__/tools/exec.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -44,4 +44,5 @@ export declare function maxInGameBudgetMs(opts?: DeriveOptions): number;
|
|
|
44
44
|
export declare function deriveTimeouts(inGameBudgetMs: number, opts?: DeriveOptions): DerivedTimeouts;
|
|
45
45
|
export declare const STEP_BUDGET_CAP_MS = 50000;
|
|
46
46
|
export declare const INPUT_BUDGET_CAP_MS = 40000;
|
|
47
|
+
export declare const EXEC_BUDGET_CAP_MS = 30000;
|
|
47
48
|
//# sourceMappingURL=timeouts.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timeouts.d.ts","sourceRoot":"","sources":["../../src/connection/timeouts.ts"],"names":[],"mappings":"AAuBA,kGAAkG;AAClG,eAAO,MAAM,gBAAgB,QAAS,CAAC;AAEvC;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,QAAS,CAAC;AAE1C,6GAA6G;AAC7G,eAAO,MAAM,aAAa,QAAS,CAAC;AAEpC,sGAAsG;AACtG,eAAO,MAAM,mBAAmB,OAAQ,CAAC;AAEzC,6EAA6E;AAC7E,eAAO,MAAM,eAAe,OAAQ,CAAC;AAErC,sEAAsE;AACtE,eAAO,MAAM,gBAAgB,OAAQ,CAAC;AAItC,MAAM,WAAW,aAAa;IAC5B,0FAA0F;IAC1F,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,oFAAoF;IACpF,YAAY,EAAE,MAAM,CAAC;IACrB,6FAA6F;IAC7F,OAAO,EAAE,MAAM,CAAC;IAChB,iGAAiG;IACjG,QAAQ,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,eAAe,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,GAAE,aAAkB,GAAG,MAAM,CAGlE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,cAAc,EAAE,MAAM,EAAE,IAAI,GAAE,aAAkB,GAAG,eAAe,CAQhG;AAOD,eAAO,MAAM,kBAAkB,QAAS,CAAC;AACzC,eAAO,MAAM,mBAAmB,QAAS,CAAC"}
|
|
1
|
+
{"version":3,"file":"timeouts.d.ts","sourceRoot":"","sources":["../../src/connection/timeouts.ts"],"names":[],"mappings":"AAuBA,kGAAkG;AAClG,eAAO,MAAM,gBAAgB,QAAS,CAAC;AAEvC;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,QAAS,CAAC;AAE1C,6GAA6G;AAC7G,eAAO,MAAM,aAAa,QAAS,CAAC;AAEpC,sGAAsG;AACtG,eAAO,MAAM,mBAAmB,OAAQ,CAAC;AAEzC,6EAA6E;AAC7E,eAAO,MAAM,eAAe,OAAQ,CAAC;AAErC,sEAAsE;AACtE,eAAO,MAAM,gBAAgB,OAAQ,CAAC;AAItC,MAAM,WAAW,aAAa;IAC5B,0FAA0F;IAC1F,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,oFAAoF;IACpF,YAAY,EAAE,MAAM,CAAC;IACrB,6FAA6F;IAC7F,OAAO,EAAE,MAAM,CAAC;IAChB,iGAAiG;IACjG,QAAQ,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,eAAe,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,GAAE,aAAkB,GAAG,MAAM,CAGlE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,cAAc,EAAE,MAAM,EAAE,IAAI,GAAE,aAAkB,GAAG,eAAe,CAQhG;AAOD,eAAO,MAAM,kBAAkB,QAAS,CAAC;AACzC,eAAO,MAAM,mBAAmB,QAAS,CAAC;AAC1C,eAAO,MAAM,kBAAkB,QAAS,CAAC"}
|
|
@@ -69,4 +69,5 @@ export function deriveTimeouts(inGameBudgetMs, opts = {}) {
|
|
|
69
69
|
// the published cap can never time out server-side.
|
|
70
70
|
export const STEP_BUDGET_CAP_MS = 50_000; // game_time step / step_until (no ready-wait)
|
|
71
71
|
export const INPUT_BUDGET_CAP_MS = 40_000; // godot_input sequence span / capture offsets / type_text duration (ready-wait)
|
|
72
|
+
export const EXEC_BUDGET_CAP_MS = 30_000; // godot_exec run (no ready-wait; scripts should be fast, the cap is patience not enforcement)
|
|
72
73
|
//# sourceMappingURL=timeouts.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timeouts.js","sourceRoot":"","sources":["../../src/connection/timeouts.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,EAAE;AACF,0EAA0E;AAC1E,qEAAqE;AACrE,6EAA6E;AAC7E,2EAA2E;AAC3E,2EAA2E;AAC3E,4EAA4E;AAC5E,uEAAuE;AACvE,EAAE;AACF,+EAA+E;AAC/E,+EAA+E;AAC/E,sDAAsD;AACtD,EAAE;AACF,oFAAoF;AACpF,wFAAwF;AACxF,yFAAyF;AACzF,wFAAwF;AACxF,EAAE;AACF,8EAA8E;AAC9E,8EAA8E;AAC9E,4CAA4C;AAE5C,kGAAkG;AAClG,MAAM,CAAC,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAEvC;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAE1C,6GAA6G;AAC7G,MAAM,CAAC,MAAM,aAAa,GAAG,MAAM,CAAC;AAEpC,sGAAsG;AACtG,MAAM,CAAC,MAAM,mBAAmB,GAAG,KAAK,CAAC;AAEzC,6EAA6E;AAC7E,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,CAAC;AAErC,sEAAsE;AACtE,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAK,CAAC;AAEtC,MAAM,iBAAiB,GAAG,mBAAmB,GAAG,eAAe,GAAG,gBAAgB,CAAC;AAkBnF;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAsB,EAAE;IACxD,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;IACjD,OAAO,mBAAmB,GAAG,iBAAiB,GAAG,KAAK,CAAC;AACzD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,cAAsB,EAAE,OAAsB,EAAE;IAC7E,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;IACjD,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/E,MAAM,eAAe,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC;IAC/E,MAAM,YAAY,GAAG,eAAe,GAAG,mBAAmB,CAAC;IAC3D,MAAM,OAAO,GAAG,KAAK,GAAG,YAAY,GAAG,eAAe,CAAC;IACvD,MAAM,QAAQ,GAAG,OAAO,GAAG,gBAAgB,CAAC;IAC5C,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,CAAC;AAC9D,CAAC;AAED,8EAA8E;AAC9E,gFAAgF;AAChF,8EAA8E;AAC9E,gFAAgF;AAChF,oDAAoD;AACpD,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAM,CAAC,CAAI,8CAA8C;AAC3F,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC,CAAG,gFAAgF"}
|
|
1
|
+
{"version":3,"file":"timeouts.js","sourceRoot":"","sources":["../../src/connection/timeouts.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,EAAE;AACF,0EAA0E;AAC1E,qEAAqE;AACrE,6EAA6E;AAC7E,2EAA2E;AAC3E,2EAA2E;AAC3E,4EAA4E;AAC5E,uEAAuE;AACvE,EAAE;AACF,+EAA+E;AAC/E,+EAA+E;AAC/E,sDAAsD;AACtD,EAAE;AACF,oFAAoF;AACpF,wFAAwF;AACxF,yFAAyF;AACzF,wFAAwF;AACxF,EAAE;AACF,8EAA8E;AAC9E,8EAA8E;AAC9E,4CAA4C;AAE5C,kGAAkG;AAClG,MAAM,CAAC,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAEvC;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAE1C,6GAA6G;AAC7G,MAAM,CAAC,MAAM,aAAa,GAAG,MAAM,CAAC;AAEpC,sGAAsG;AACtG,MAAM,CAAC,MAAM,mBAAmB,GAAG,KAAK,CAAC;AAEzC,6EAA6E;AAC7E,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,CAAC;AAErC,sEAAsE;AACtE,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAK,CAAC;AAEtC,MAAM,iBAAiB,GAAG,mBAAmB,GAAG,eAAe,GAAG,gBAAgB,CAAC;AAkBnF;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAsB,EAAE;IACxD,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;IACjD,OAAO,mBAAmB,GAAG,iBAAiB,GAAG,KAAK,CAAC;AACzD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,cAAsB,EAAE,OAAsB,EAAE;IAC7E,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;IACjD,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/E,MAAM,eAAe,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC;IAC/E,MAAM,YAAY,GAAG,eAAe,GAAG,mBAAmB,CAAC;IAC3D,MAAM,OAAO,GAAG,KAAK,GAAG,YAAY,GAAG,eAAe,CAAC;IACvD,MAAM,QAAQ,GAAG,OAAO,GAAG,gBAAgB,CAAC;IAC5C,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,CAAC;AAC9D,CAAC;AAED,8EAA8E;AAC9E,gFAAgF;AAChF,8EAA8E;AAC9E,gFAAgF;AAChF,oDAAoD;AACpD,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAM,CAAC,CAAI,8CAA8C;AAC3F,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC,CAAG,gFAAgF;AAC7H,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAM,CAAC,CAAI,8FAA8F"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { AnyToolDefinition } from '../core/types.js';
|
|
3
|
+
export declare const exec: import("../core/types.js").ToolDefinition<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
4
|
+
action: z.ZodLiteral<"run">;
|
|
5
|
+
source: z.ZodString;
|
|
6
|
+
budget_ms: z.ZodOptional<z.ZodNumber>;
|
|
7
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
8
|
+
action: z.ZodLiteral<"list">;
|
|
9
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
10
|
+
action: z.ZodLiteral<"remove">;
|
|
11
|
+
name: z.ZodString;
|
|
12
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
13
|
+
action: z.ZodLiteral<"clear">;
|
|
14
|
+
}, z.core.$strip>], "action">>;
|
|
15
|
+
export declare const execTools: AnyToolDefinition[];
|
|
16
|
+
//# sourceMappingURL=exec.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exec.d.ts","sourceRoot":"","sources":["../../src/tools/exec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AA6E1D,eAAO,MAAM,IAAI;;;;;;;;;;;8BAuDf,CAAC;AAEH,eAAO,MAAM,SAAS,EAAa,iBAAiB,EAAE,CAAC"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { defineTool } from '../core/define-tool.js';
|
|
3
|
+
import { structured } from '../core/structured.js';
|
|
4
|
+
import { deriveTimeouts, EXEC_BUDGET_CAP_MS } from '../connection/timeouts.js';
|
|
5
|
+
// Generous for a script, tiny for the transport: the editor websocket moves
|
|
6
|
+
// multi-megabyte screenshots, so this cap is about keeping exec calls
|
|
7
|
+
// reviewable, not about what the wire can carry.
|
|
8
|
+
const EXEC_SOURCE_MAX_CHARS = 16_384;
|
|
9
|
+
const EXEC_DEFAULT_BUDGET_MS = 10_000;
|
|
10
|
+
const ExecSchema = z.discriminatedUnion('action', [
|
|
11
|
+
z.object({
|
|
12
|
+
action: z
|
|
13
|
+
.literal('run')
|
|
14
|
+
.describe('Run one-shot GDScript inside the running game and return its value'),
|
|
15
|
+
source: z
|
|
16
|
+
.string()
|
|
17
|
+
.min(1)
|
|
18
|
+
.max(EXEC_SOURCE_MAX_CHARS)
|
|
19
|
+
.describe('GDScript statements, compiled as a function body. In scope: every autoload by its own ' +
|
|
20
|
+
'name (e.g. `G.wave = 5`), `tree` (SceneTree), `root` (root Window) — the same context as ' +
|
|
21
|
+
'step_until predicates — plus `holder`, a Node that survives scene reloads: attach nodes ' +
|
|
22
|
+
'under it for behavior that persists between tool calls (a Timer-driven guard, an autofire ' +
|
|
23
|
+
'bot), then manage them with list/remove/clear. Holder children pause with the tree, so a ' +
|
|
24
|
+
'bot armed under a freeze acts only after thaw/step. Use an explicit `return` to get a ' +
|
|
25
|
+
'value back (there is no implicit return): primitives (bool/int/float/String) come back ' +
|
|
26
|
+
'intact; any other type (Array/Dictionary/Object/Vector2) comes back as a str() preview ' +
|
|
27
|
+
'TRUNCATED to 200 chars — return JSON.stringify(...) yourself when you need structure. ' +
|
|
28
|
+
'print() output is not returned — use return values, or minimal-godot-mcp ' +
|
|
29
|
+
'get_console_output. Function bodies cannot declare top-level func/class — use lambdas ' +
|
|
30
|
+
'for callbacks, or build a sub-script with GDScript.new() and set_script() it onto a ' +
|
|
31
|
+
'holder child for _process-driven behavior. No `await` (synchronous-only; compose with ' +
|
|
32
|
+
'godot_game_time to wait). A runtime error or failed assert() breaks the game into the ' +
|
|
33
|
+
'editor debugger mid-call; the relay auto-resumes it and the error comes back in ' +
|
|
34
|
+
'runtime_errors (any debugger break in the call window is resumed, including a breakpoint ' +
|
|
35
|
+
'hit by unrelated game code).'),
|
|
36
|
+
budget_ms: z
|
|
37
|
+
.number()
|
|
38
|
+
.int()
|
|
39
|
+
.min(1)
|
|
40
|
+
.max(EXEC_BUDGET_CAP_MS)
|
|
41
|
+
.optional()
|
|
42
|
+
.describe(`Wall-clock patience for the call, used to size the timeout cascade (default ${EXEC_DEFAULT_BUDGET_MS}, ` +
|
|
43
|
+
`max ${EXEC_BUDGET_CAP_MS}). NOT enforcement: a synchronous script cannot be preempted, so an ` +
|
|
44
|
+
'infinite loop hangs the game past any budget — recover with godot_editor stop.'),
|
|
45
|
+
}),
|
|
46
|
+
z.object({
|
|
47
|
+
action: z
|
|
48
|
+
.literal('list')
|
|
49
|
+
.describe('List the nodes currently attached under the exec holder (name, class, age, processing state)'),
|
|
50
|
+
}),
|
|
51
|
+
z.object({
|
|
52
|
+
action: z.literal('remove').describe('Remove one exec holder child by name (queue_free)'),
|
|
53
|
+
name: z.string().min(1).describe('Node name as reported by list'),
|
|
54
|
+
}),
|
|
55
|
+
z.object({
|
|
56
|
+
action: z.literal('clear').describe('Remove every exec holder child (queue_free all)'),
|
|
57
|
+
}),
|
|
58
|
+
]);
|
|
59
|
+
export const exec = defineTool({
|
|
60
|
+
name: 'godot_exec',
|
|
61
|
+
annotations: { title: 'Game Script Execution', readOnlyHint: false, destructiveHint: true, openWorldHint: false },
|
|
62
|
+
description: 'Execute GDScript inside the RUNNING game process — the scenario-setup primitive: grant ' +
|
|
63
|
+
'weapons, skip waves, spawn entities, arm persistent test bots, without baking debug hooks ' +
|
|
64
|
+
'into game code. Errors when no game is running. For launch-time setup, compose: godot_editor ' +
|
|
65
|
+
'run frozen=true -> godot_exec run (mutate state, attach bots under `holder`) -> ' +
|
|
66
|
+
'godot_game_time thaw. A static denylist rejects accidental process/file-write escape ' +
|
|
67
|
+
'(OS.execute, DirAccess, write-mode FileAccess, ResourceSaver, ProjectSettings.save, ...) and ' +
|
|
68
|
+
'names the offending token — an accident guard, NOT a security boundary. Compile errors ' +
|
|
69
|
+
'reject the call with the parser message; runtime errors come back in runtime_errors with the ' +
|
|
70
|
+
'call still completing.',
|
|
71
|
+
schema: ExecSchema,
|
|
72
|
+
async execute(args, { godot }) {
|
|
73
|
+
switch (args.action) {
|
|
74
|
+
case 'run': {
|
|
75
|
+
// Same cascade class as game_time (no bridge-ready wait), but no
|
|
76
|
+
// wall_budget_ms is pushed: the bridge cannot abort a synchronous
|
|
77
|
+
// script mid-flight, and pretending it enforces a wall would be
|
|
78
|
+
// dishonest. The stagger still holds — the relay and this socket
|
|
79
|
+
// simply wait budget-long before declaring the script hung.
|
|
80
|
+
const t = deriveTimeouts(args.budget_ms ?? EXEC_DEFAULT_BUDGET_MS);
|
|
81
|
+
const result = await godot.sendCommand('exec_run', { source: args.source, relay_timeout_ms: t.relayMs }, { timeoutMs: t.serverMs });
|
|
82
|
+
return structured(result);
|
|
83
|
+
}
|
|
84
|
+
case 'list': {
|
|
85
|
+
const result = await godot.sendCommand('exec_list');
|
|
86
|
+
// Structured in both branches: a result whose SHAPE depends on runtime
|
|
87
|
+
// data would make the empty case unparseable for structured readers.
|
|
88
|
+
return structured(result);
|
|
89
|
+
}
|
|
90
|
+
case 'remove': {
|
|
91
|
+
const result = await godot.sendCommand('exec_remove', { name: args.name });
|
|
92
|
+
return structured(result);
|
|
93
|
+
}
|
|
94
|
+
case 'clear': {
|
|
95
|
+
const result = await godot.sendCommand('exec_clear');
|
|
96
|
+
return `Removed ${result.removed_count} exec node(s).`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
export const execTools = [exec];
|
|
102
|
+
//# sourceMappingURL=exec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exec.js","sourceRoot":"","sources":["../../src/tools/exec.ts"],"names":[],"mappings":"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"}
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -12,4 +12,5 @@ export { inputTools } from './input.js';
|
|
|
12
12
|
export { profilerTools } from './profiler.js';
|
|
13
13
|
export { runtimeStateTools } from './runtime-state.js';
|
|
14
14
|
export { gameTimeTools } from './game-time.js';
|
|
15
|
+
export { execTools } from './exec.js';
|
|
15
16
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAgBA,wBAAgB,gBAAgB,IAAI,IAAI,CAevC;AAED,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC"}
|
package/dist/tools/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { inputTools } from './input.js';
|
|
|
12
12
|
import { profilerTools } from './profiler.js';
|
|
13
13
|
import { runtimeStateTools } from './runtime-state.js';
|
|
14
14
|
import { gameTimeTools } from './game-time.js';
|
|
15
|
+
import { execTools } from './exec.js';
|
|
15
16
|
export function registerAllTools() {
|
|
16
17
|
registry.registerTools(sceneTools);
|
|
17
18
|
registry.registerTools(nodeTools);
|
|
@@ -26,6 +27,7 @@ export function registerAllTools() {
|
|
|
26
27
|
registry.registerTools(profilerTools);
|
|
27
28
|
registry.registerTools(runtimeStateTools);
|
|
28
29
|
registry.registerTools(gameTimeTools);
|
|
30
|
+
registry.registerTools(execTools);
|
|
29
31
|
}
|
|
30
32
|
export { sceneTools } from './scene.js';
|
|
31
33
|
export { nodeTools } from './node.js';
|
|
@@ -40,4 +42,5 @@ export { inputTools } from './input.js';
|
|
|
40
42
|
export { profilerTools } from './profiler.js';
|
|
41
43
|
export { runtimeStateTools } from './runtime-state.js';
|
|
42
44
|
export { gameTimeTools } from './game-time.js';
|
|
45
|
+
export { execTools } from './exec.js';
|
|
43
46
|
//# sourceMappingURL=index.js.map
|
package/dist/tools/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC,MAAM,UAAU,gBAAgB;IAC9B,QAAQ,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC;IACnC,QAAQ,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC;IAClC,QAAQ,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC;IACpC,QAAQ,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC;IACrC,QAAQ,CAAC,aAAa,CAAC,cAAc,CAAC,CAAC;IACvC,QAAQ,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC;IACrC,QAAQ,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC;IACtC,QAAQ,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC;IACrC,QAAQ,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC;IAClC,QAAQ,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC;IACnC,QAAQ,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC;IACtC,QAAQ,CAAC,aAAa,CAAC,iBAAiB,CAAC,CAAC;IAC1C,QAAQ,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC;IACtC,QAAQ,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC;AACpC,CAAC;AAED,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC"}
|