@openlucaskaka/kagent 0.1.7
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/README.md +353 -0
- package/npm/bin/kagent-serve.js +6 -0
- package/npm/bin/kagent.js +6 -0
- package/npm/lib/App.js +524 -0
- package/npm/lib/app-state.js +224 -0
- package/npm/lib/approval-choice.js +25 -0
- package/npm/lib/commands.js +59 -0
- package/npm/lib/editor.js +188 -0
- package/npm/lib/ink-runner.js +41 -0
- package/npm/lib/kagent-home.js +39 -0
- package/npm/lib/launcher.js +221 -0
- package/npm/lib/protocol.js +33 -0
- package/npm/lib/provider-setup.js +139 -0
- package/npm/lib/python-runner.js +892 -0
- package/npm/lib/runtime-client.js +390 -0
- package/npm/lib/terminal-input.js +127 -0
- package/npm/lib/terminal-text.js +19 -0
- package/npm/lib/terminal-width.js +14 -0
- package/npm/lib/transcript.js +227 -0
- package/npm/lib/ui-components.js +247 -0
- package/npm/lib/update-manager.js +334 -0
- package/package.json +39 -0
- package/pyproject.toml +55 -0
- package/src/kagent/__init__.py +90 -0
- package/src/kagent/cli/__init__.py +5 -0
- package/src/kagent/cli/__main__.py +6 -0
- package/src/kagent/cli/commands.py +112 -0
- package/src/kagent/cli/conversation.py +127 -0
- package/src/kagent/cli/interactive.py +841 -0
- package/src/kagent/cli/main.py +685 -0
- package/src/kagent/cli/memory.py +460 -0
- package/src/kagent/cli/pending_approval.py +169 -0
- package/src/kagent/cli/provider.py +27 -0
- package/src/kagent/cli/session_commands.py +401 -0
- package/src/kagent/cli/stdio_runtime.py +784 -0
- package/src/kagent/cli/trace.py +67 -0
- package/src/kagent/cli/ui.py +931 -0
- package/src/kagent/core/__init__.py +0 -0
- package/src/kagent/core/agent.py +296 -0
- package/src/kagent/core/faults.py +11 -0
- package/src/kagent/core/invariants.py +47 -0
- package/src/kagent/core/normalization.py +81 -0
- package/src/kagent/core/planning.py +48 -0
- package/src/kagent/core/state.py +73 -0
- package/src/kagent/core/summary.py +64 -0
- package/src/kagent/core/tools.py +196 -0
- package/src/kagent/core/trace.py +57 -0
- package/src/kagent/eval/__init__.py +11 -0
- package/src/kagent/eval/cases.py +229 -0
- package/src/kagent/eval/evaluator.py +216 -0
- package/src/kagent/integrations/__init__.py +3 -0
- package/src/kagent/integrations/audit.py +95 -0
- package/src/kagent/integrations/backends.py +131 -0
- package/src/kagent/integrations/memory.py +301 -0
- package/src/kagent/ops/__init__.py +0 -0
- package/src/kagent/ops/batch.py +156 -0
- package/src/kagent/ops/doctor.py +255 -0
- package/src/kagent/ops/metrics.py +214 -0
- package/src/kagent/ops/release_evidence.py +877 -0
- package/src/kagent/ops/release_manifest.py +142 -0
- package/src/kagent/ops/trace_replay.py +285 -0
- package/src/kagent/providers/__init__.py +7 -0
- package/src/kagent/providers/embeddings.py +187 -0
- package/src/kagent/providers/llm.py +770 -0
- package/src/kagent/py.typed +1 -0
- package/src/kagent/runtime/__init__.py +28 -0
- package/src/kagent/runtime/action_graph.py +543 -0
- package/src/kagent/runtime/agent.py +2089 -0
- package/src/kagent/runtime/approval.py +64 -0
- package/src/kagent/runtime/cancellation.py +64 -0
- package/src/kagent/runtime/checkpoint_state.py +146 -0
- package/src/kagent/runtime/checkpoint_storage.py +270 -0
- package/src/kagent/runtime/context.py +65 -0
- package/src/kagent/runtime/file_transaction.py +195 -0
- package/src/kagent/runtime/hooks.py +74 -0
- package/src/kagent/runtime/metadata.py +116 -0
- package/src/kagent/runtime/patch_checkpoints.py +385 -0
- package/src/kagent/runtime/policy.py +50 -0
- package/src/kagent/runtime/presentation.py +205 -0
- package/src/kagent/runtime/redaction.py +130 -0
- package/src/kagent/runtime/sandbox.py +331 -0
- package/src/kagent/runtime/skills.py +66 -0
- package/src/kagent/runtime/steering.py +56 -0
- package/src/kagent/runtime/steps.py +255 -0
- package/src/kagent/runtime/task_state.py +40 -0
- package/src/kagent/runtime/tools.py +3532 -0
- package/src/kagent/runtime/types.py +240 -0
- package/src/kagent/runtime/workspace.py +597 -0
- package/src/kagent/service/__init__.py +26 -0
- package/src/kagent/service/__main__.py +6 -0
- package/src/kagent/service/active_runs.py +178 -0
- package/src/kagent/service/cli.py +737 -0
- package/src/kagent/service/contract.py +2571 -0
- package/src/kagent/service/errors.py +71 -0
- package/src/kagent/service/idempotency.py +584 -0
- package/src/kagent/service/router.py +884 -0
- package/src/kagent/service/run.py +150 -0
- package/src/kagent/service/runtime.py +1915 -0
- package/src/kagent/service/runtime_approval.py +20 -0
- package/src/kagent/service/runtime_cancel.py +192 -0
- package/src/kagent/service/runtime_lifecycle.py +229 -0
- package/src/kagent/service/runtime_metadata.py +21 -0
- package/src/kagent/service/runtime_policy.py +115 -0
- package/src/kagent/service/runtime_recovery.py +573 -0
- package/src/kagent/service/runtime_resume.py +382 -0
- package/src/kagent/service/runtime_resume_claim.py +116 -0
- package/src/kagent/service/runtime_run.py +350 -0
- package/src/kagent/service/runtime_status.py +2007 -0
- package/src/kagent/service/safety.py +139 -0
- package/src/kagent/service/server.py +114 -0
- package/src/kagent/service/status.py +233 -0
- package/src/kagent/service/trace_store.py +322 -0
- package/src/kagent/service/transport.py +24 -0
- package/src/kagent/utils/__init__.py +0 -0
- package/src/kagent/utils/config_validation.py +21 -0
- package/src/kagent/utils/json_output.py +23 -0
- package/src/kagent/utils/paths.py +473 -0
|
@@ -0,0 +1,892 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const childProcess = require("child_process");
|
|
4
|
+
const crypto = require("crypto");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
|
|
8
|
+
const { kagentCachePath, resolveKagentHome } = require("./kagent-home");
|
|
9
|
+
|
|
10
|
+
const TEMP_RUNTIME_STALE_MS = 24 * 60 * 60 * 1000;
|
|
11
|
+
const PYTHON_ENTRYPOINTS = Object.freeze({
|
|
12
|
+
kagent: "import sys; sys.argv = sys.argv[1:]; from kagent.cli import main; raise SystemExit(main())",
|
|
13
|
+
"kagent-serve": "import sys; sys.argv = sys.argv[1:]; from kagent.service import main; raise SystemExit(main())"
|
|
14
|
+
});
|
|
15
|
+
const SECURE_FILESYSTEM_HELPER = String.raw`
|
|
16
|
+
import ctypes
|
|
17
|
+
import errno
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import secrets
|
|
21
|
+
import stat
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
DIRECTORY_FLAGS = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def fail(message):
|
|
29
|
+
raise RuntimeError(message)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def path_parts(target):
|
|
33
|
+
if not os.path.isabs(target):
|
|
34
|
+
fail("managed path must be absolute")
|
|
35
|
+
normalized = os.path.normpath(target)
|
|
36
|
+
parts = [part for part in normalized.split(os.sep) if part]
|
|
37
|
+
if not parts:
|
|
38
|
+
fail("refusing managed filesystem root")
|
|
39
|
+
return parts
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def open_directory(target, create_missing):
|
|
43
|
+
parts = path_parts(target)
|
|
44
|
+
current_fd = os.open(os.sep, DIRECTORY_FLAGS)
|
|
45
|
+
try:
|
|
46
|
+
for part in parts:
|
|
47
|
+
try:
|
|
48
|
+
next_fd = os.open(part, DIRECTORY_FLAGS, dir_fd=current_fd)
|
|
49
|
+
except FileNotFoundError:
|
|
50
|
+
if not create_missing:
|
|
51
|
+
raise
|
|
52
|
+
try:
|
|
53
|
+
os.mkdir(part, 0o700, dir_fd=current_fd)
|
|
54
|
+
except FileExistsError:
|
|
55
|
+
pass
|
|
56
|
+
next_fd = os.open(part, DIRECTORY_FLAGS, dir_fd=current_fd)
|
|
57
|
+
except OSError as error:
|
|
58
|
+
if error.errno in (errno.ELOOP, errno.ENOTDIR):
|
|
59
|
+
fail(f"refusing symbolic link or non-directory in managed path: {target}")
|
|
60
|
+
raise
|
|
61
|
+
os.close(current_fd)
|
|
62
|
+
current_fd = next_fd
|
|
63
|
+
return current_fd
|
|
64
|
+
except BaseException:
|
|
65
|
+
os.close(current_fd)
|
|
66
|
+
raise
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def ensure_directory(target):
|
|
70
|
+
directory_fd = open_directory(target, True)
|
|
71
|
+
try:
|
|
72
|
+
if not stat.S_ISDIR(os.fstat(directory_fd).st_mode):
|
|
73
|
+
fail(f"managed path is not a directory: {target}")
|
|
74
|
+
os.fchmod(directory_fd, 0o700)
|
|
75
|
+
finally:
|
|
76
|
+
os.close(directory_fd)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def write_file(target):
|
|
80
|
+
parent, name = os.path.split(os.path.normpath(target))
|
|
81
|
+
if not name or name in (".", ".."):
|
|
82
|
+
fail(f"managed path is not a file: {target}")
|
|
83
|
+
parent_fd = open_directory(parent, False)
|
|
84
|
+
file_fd = None
|
|
85
|
+
try:
|
|
86
|
+
try:
|
|
87
|
+
file_fd = os.open(
|
|
88
|
+
name,
|
|
89
|
+
os.O_WRONLY | os.O_CREAT | os.O_TRUNC | os.O_NOFOLLOW,
|
|
90
|
+
0o600,
|
|
91
|
+
dir_fd=parent_fd,
|
|
92
|
+
)
|
|
93
|
+
except OSError as error:
|
|
94
|
+
if error.errno in (errno.ELOOP, errno.ENOTDIR):
|
|
95
|
+
fail(f"refusing symbolic link in managed path: {target}")
|
|
96
|
+
raise
|
|
97
|
+
if not stat.S_ISREG(os.fstat(file_fd).st_mode):
|
|
98
|
+
fail(f"managed path is not a file: {target}")
|
|
99
|
+
os.fchmod(file_fd, 0o600)
|
|
100
|
+
body = sys.stdin.buffer.read()
|
|
101
|
+
offset = 0
|
|
102
|
+
while offset < len(body):
|
|
103
|
+
offset += os.write(file_fd, body[offset:])
|
|
104
|
+
os.fchmod(file_fd, 0o600)
|
|
105
|
+
finally:
|
|
106
|
+
if file_fd is not None:
|
|
107
|
+
os.close(file_fd)
|
|
108
|
+
os.close(parent_fd)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def managed_parent(target):
|
|
112
|
+
parent, name = os.path.split(os.path.normpath(target))
|
|
113
|
+
if not name or name in (".", ".."):
|
|
114
|
+
fail(f"managed path has no valid name: {target}")
|
|
115
|
+
return open_directory(parent, False), name
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def directory_stat(parent_fd, name, target):
|
|
119
|
+
try:
|
|
120
|
+
value = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
|
|
121
|
+
except FileNotFoundError:
|
|
122
|
+
return None
|
|
123
|
+
if stat.S_ISLNK(value.st_mode):
|
|
124
|
+
fail(f"refusing symbolic link in managed path: {target}")
|
|
125
|
+
if not stat.S_ISDIR(value.st_mode):
|
|
126
|
+
fail(f"managed path is not a directory: {target}")
|
|
127
|
+
return value
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def validate_directory(target):
|
|
131
|
+
directory_fd = open_directory(target, False)
|
|
132
|
+
try:
|
|
133
|
+
if not stat.S_ISDIR(os.fstat(directory_fd).st_mode):
|
|
134
|
+
fail(f"managed path is not a directory: {target}")
|
|
135
|
+
finally:
|
|
136
|
+
os.close(directory_fd)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def create_temp_directory(final_target):
|
|
140
|
+
parent_fd, final_name = managed_parent(final_target)
|
|
141
|
+
try:
|
|
142
|
+
for _attempt in range(100):
|
|
143
|
+
name = f"t{secrets.token_hex(31)}{secrets.choice('0123456789abcdef')}"
|
|
144
|
+
try:
|
|
145
|
+
os.mkdir(name, 0o700, dir_fd=parent_fd)
|
|
146
|
+
except FileExistsError:
|
|
147
|
+
continue
|
|
148
|
+
directory_fd = os.open(name, DIRECTORY_FLAGS, dir_fd=parent_fd)
|
|
149
|
+
try:
|
|
150
|
+
os.fchmod(directory_fd, 0o700)
|
|
151
|
+
finally:
|
|
152
|
+
os.close(directory_fd)
|
|
153
|
+
return os.path.join(os.path.dirname(os.path.normpath(final_target)), name)
|
|
154
|
+
fail(f"unable to allocate temporary runtime directory for {final_target}")
|
|
155
|
+
finally:
|
|
156
|
+
os.close(parent_fd)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def remove_directory_contents(directory_fd):
|
|
160
|
+
try:
|
|
161
|
+
names = os.listdir(directory_fd)
|
|
162
|
+
except FileNotFoundError:
|
|
163
|
+
return
|
|
164
|
+
for name in names:
|
|
165
|
+
try:
|
|
166
|
+
value = os.stat(name, dir_fd=directory_fd, follow_symlinks=False)
|
|
167
|
+
except FileNotFoundError:
|
|
168
|
+
continue
|
|
169
|
+
if stat.S_ISDIR(value.st_mode):
|
|
170
|
+
try:
|
|
171
|
+
child_fd = os.open(name, DIRECTORY_FLAGS, dir_fd=directory_fd)
|
|
172
|
+
except FileNotFoundError:
|
|
173
|
+
continue
|
|
174
|
+
except OSError as error:
|
|
175
|
+
if error.errno in (errno.ELOOP, errno.ENOTDIR):
|
|
176
|
+
fail(f"temporary runtime directory changed type: {name}")
|
|
177
|
+
raise
|
|
178
|
+
try:
|
|
179
|
+
remove_directory_contents(child_fd)
|
|
180
|
+
finally:
|
|
181
|
+
os.close(child_fd)
|
|
182
|
+
try:
|
|
183
|
+
os.rmdir(name, dir_fd=directory_fd)
|
|
184
|
+
except FileNotFoundError:
|
|
185
|
+
continue
|
|
186
|
+
else:
|
|
187
|
+
try:
|
|
188
|
+
os.unlink(name, dir_fd=directory_fd)
|
|
189
|
+
except FileNotFoundError:
|
|
190
|
+
continue
|
|
191
|
+
except IsADirectoryError:
|
|
192
|
+
fail(f"temporary runtime entry changed type: {name}")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def remove_tree_entry(parent_fd, name, target):
|
|
196
|
+
try:
|
|
197
|
+
directory_fd = os.open(name, DIRECTORY_FLAGS, dir_fd=parent_fd)
|
|
198
|
+
except FileNotFoundError:
|
|
199
|
+
return
|
|
200
|
+
except OSError as error:
|
|
201
|
+
if error.errno in (errno.ELOOP, errno.ENOTDIR):
|
|
202
|
+
fail(f"refusing symbolic link or non-directory temporary runtime: {target}")
|
|
203
|
+
raise
|
|
204
|
+
try:
|
|
205
|
+
remove_directory_contents(directory_fd)
|
|
206
|
+
finally:
|
|
207
|
+
os.close(directory_fd)
|
|
208
|
+
try:
|
|
209
|
+
os.rmdir(name, dir_fd=parent_fd)
|
|
210
|
+
except FileNotFoundError:
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def remove_tree(target):
|
|
215
|
+
parent_fd, name = managed_parent(target)
|
|
216
|
+
try:
|
|
217
|
+
remove_tree_entry(parent_fd, name, target)
|
|
218
|
+
finally:
|
|
219
|
+
os.close(parent_fd)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def cleanup_temp_directories(parent_target, stale_before_ms):
|
|
223
|
+
parent_fd = open_directory(parent_target, False)
|
|
224
|
+
try:
|
|
225
|
+
for name in os.listdir(parent_fd):
|
|
226
|
+
if re.fullmatch(r"t[0-9a-f]{63}", name) is None:
|
|
227
|
+
continue
|
|
228
|
+
target = os.path.join(parent_target, name)
|
|
229
|
+
try:
|
|
230
|
+
value = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
|
|
231
|
+
except FileNotFoundError:
|
|
232
|
+
continue
|
|
233
|
+
if stat.S_ISLNK(value.st_mode):
|
|
234
|
+
fail(f"refusing symbolic link temporary runtime: {target}")
|
|
235
|
+
if not stat.S_ISDIR(value.st_mode):
|
|
236
|
+
fail(f"temporary runtime is not a directory: {target}")
|
|
237
|
+
if value.st_mtime * 1000 >= stale_before_ms:
|
|
238
|
+
continue
|
|
239
|
+
remove_tree_entry(parent_fd, name, target)
|
|
240
|
+
finally:
|
|
241
|
+
os.close(parent_fd)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def rename_no_replace(parent_fd, source_name, target_name, platform_name):
|
|
245
|
+
libc = ctypes.CDLL(None, use_errno=True)
|
|
246
|
+
source = os.fsencode(source_name)
|
|
247
|
+
target = os.fsencode(target_name)
|
|
248
|
+
if platform_name == "darwin":
|
|
249
|
+
rename = libc.renameatx_np
|
|
250
|
+
rename.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_uint]
|
|
251
|
+
rename.restype = ctypes.c_int
|
|
252
|
+
result = rename(parent_fd, source, parent_fd, target, 0x00000004)
|
|
253
|
+
elif platform_name.startswith("linux") and hasattr(libc, "renameat2"):
|
|
254
|
+
rename = libc.renameat2
|
|
255
|
+
rename.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_uint]
|
|
256
|
+
rename.restype = ctypes.c_int
|
|
257
|
+
result = rename(parent_fd, source, parent_fd, target, 1)
|
|
258
|
+
else:
|
|
259
|
+
fail(f"unsupported atomic publish platform: {platform_name}")
|
|
260
|
+
if result == 0:
|
|
261
|
+
return True
|
|
262
|
+
error_number = ctypes.get_errno()
|
|
263
|
+
if error_number in (errno.EEXIST, errno.ENOTEMPTY):
|
|
264
|
+
return False
|
|
265
|
+
raise OSError(error_number, os.strerror(error_number))
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def publish_directory(temp_target, final_target, platform_name):
|
|
269
|
+
temp_parent = os.path.dirname(os.path.normpath(temp_target))
|
|
270
|
+
final_parent = os.path.dirname(os.path.normpath(final_target))
|
|
271
|
+
if temp_parent != final_parent:
|
|
272
|
+
fail("temporary and final runtime directories must share a parent")
|
|
273
|
+
parent_fd = open_directory(final_parent, False)
|
|
274
|
+
try:
|
|
275
|
+
temp_name = os.path.basename(temp_target)
|
|
276
|
+
final_name = os.path.basename(final_target)
|
|
277
|
+
directory_stat(parent_fd, temp_name, temp_target)
|
|
278
|
+
if rename_no_replace(parent_fd, temp_name, final_name, platform_name):
|
|
279
|
+
return "published"
|
|
280
|
+
directory_stat(parent_fd, final_name, final_target)
|
|
281
|
+
return "exists"
|
|
282
|
+
finally:
|
|
283
|
+
os.close(parent_fd)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
operation, target = sys.argv[1:3]
|
|
288
|
+
if operation == "ensure-directory":
|
|
289
|
+
ensure_directory(target)
|
|
290
|
+
elif operation == "write-file":
|
|
291
|
+
write_file(target)
|
|
292
|
+
elif operation == "validate-directory":
|
|
293
|
+
validate_directory(target)
|
|
294
|
+
elif operation == "create-temp-directory":
|
|
295
|
+
sys.stdout.write(create_temp_directory(target))
|
|
296
|
+
elif operation == "remove-tree":
|
|
297
|
+
remove_tree(target)
|
|
298
|
+
elif operation == "cleanup-temp-directories":
|
|
299
|
+
cleanup_temp_directories(target, int(sys.argv[3]))
|
|
300
|
+
elif operation == "publish-directory":
|
|
301
|
+
platform_name = sys.argv[4] if len(sys.argv) > 4 and sys.argv[4] else sys.platform
|
|
302
|
+
sys.stdout.write(publish_directory(target, sys.argv[3], platform_name))
|
|
303
|
+
else:
|
|
304
|
+
fail(f"unsupported secure filesystem operation: {operation}")
|
|
305
|
+
except BaseException as error:
|
|
306
|
+
sys.stderr.write(f"{error}\n")
|
|
307
|
+
raise SystemExit(1)
|
|
308
|
+
`;
|
|
309
|
+
|
|
310
|
+
function packageRoot() {
|
|
311
|
+
return path.resolve(__dirname, "..", "..");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function readPackageVersion(root) {
|
|
315
|
+
const packageJsonPath = path.join(root, "package.json");
|
|
316
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
317
|
+
return packageJson.version;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function maybePrintNodeHandledOutput(commandName, args, version) {
|
|
321
|
+
if (commandName !== "kagent") {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
const versionOutput = versionOutputTarget(args);
|
|
325
|
+
if (versionOutput === null) {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
const body = `${JSON.stringify({ version }, null, 2)}\n`;
|
|
329
|
+
if (versionOutput) {
|
|
330
|
+
fs.writeFileSync(versionOutput, body, { encoding: "utf8" });
|
|
331
|
+
} else {
|
|
332
|
+
process.stdout.write(body);
|
|
333
|
+
}
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function versionOutputTarget(args) {
|
|
338
|
+
if (args.length === 1 && args[0] === "--version") {
|
|
339
|
+
return "";
|
|
340
|
+
}
|
|
341
|
+
if (args.length === 3 && args[0] === "--version" && args[1] === "--output") {
|
|
342
|
+
return args[2];
|
|
343
|
+
}
|
|
344
|
+
if (args.length === 3 && args[0] === "--output" && args[2] === "--version") {
|
|
345
|
+
return args[1];
|
|
346
|
+
}
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function cacheRoot(env = process.env) {
|
|
351
|
+
if (env.KAGENT_NODE_VENV) {
|
|
352
|
+
return path.resolve(env.KAGENT_NODE_VENV);
|
|
353
|
+
}
|
|
354
|
+
return kagentCachePath("npm-python", env);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function metadataCacheRoot(env = process.env) {
|
|
358
|
+
return path.dirname(kagentCachePath("npm-python", env));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function updateStatePath(env = process.env) {
|
|
362
|
+
return path.join(metadataCacheRoot(env), "npm-self-update.json");
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function rejectSymlinks(targetPath) {
|
|
366
|
+
const resolved = path.resolve(targetPath);
|
|
367
|
+
const parsed = path.parse(resolved);
|
|
368
|
+
const parts = resolved.slice(parsed.root.length).split(path.sep).filter(Boolean);
|
|
369
|
+
let current = parsed.root;
|
|
370
|
+
for (const part of parts) {
|
|
371
|
+
current = path.join(current, part);
|
|
372
|
+
let stat;
|
|
373
|
+
try {
|
|
374
|
+
stat = fs.lstatSync(current);
|
|
375
|
+
} catch (error) {
|
|
376
|
+
if (error.code === "ENOENT") {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
381
|
+
if (stat.isSymbolicLink()) {
|
|
382
|
+
throw new Error(`refusing symbolic link in managed path: ${current}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function ensurePrivateDirectory(directory) {
|
|
388
|
+
const resolved = path.resolve(directory);
|
|
389
|
+
runSecureFilesystemOperation("ensure-directory", resolved);
|
|
390
|
+
return directory;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function ensureCacheRoot(env = process.env) {
|
|
394
|
+
if (env.KAGENT_NODE_VENV) {
|
|
395
|
+
return ensurePrivateDirectory(path.resolve(env.KAGENT_NODE_VENV));
|
|
396
|
+
}
|
|
397
|
+
const home = ensurePrivateDirectory(resolveKagentHome(env));
|
|
398
|
+
const cache = ensurePrivateDirectory(path.join(home, "cache"));
|
|
399
|
+
return ensurePrivateDirectory(path.join(cache, "npm-python"));
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function ensureMetadataCacheRoot(env = process.env) {
|
|
403
|
+
const home = ensurePrivateDirectory(resolveKagentHome(env));
|
|
404
|
+
return ensurePrivateDirectory(path.join(home, "cache"));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function writePrivateFile(filePath, body) {
|
|
408
|
+
runSecureFilesystemOperation("write-file", path.resolve(filePath), body);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function assertSupportedPlatform(platform = process.platform) {
|
|
412
|
+
if (platform !== "darwin" && platform !== "linux") {
|
|
413
|
+
throw new Error(`kagent npm Python runtime supports only macOS and Linux, not ${platform}`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function runSecureFilesystemOperation(operation, targetPath, body = "", extraArgs = []) {
|
|
418
|
+
assertSupportedPlatform();
|
|
419
|
+
const python = findPython();
|
|
420
|
+
const result = childProcess.spawnSync(
|
|
421
|
+
python,
|
|
422
|
+
["-c", SECURE_FILESYSTEM_HELPER, operation, targetPath].concat(extraArgs),
|
|
423
|
+
{
|
|
424
|
+
encoding: "utf8",
|
|
425
|
+
env: process.env,
|
|
426
|
+
input: body,
|
|
427
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
428
|
+
}
|
|
429
|
+
);
|
|
430
|
+
if (result.error) {
|
|
431
|
+
throw result.error;
|
|
432
|
+
}
|
|
433
|
+
if (result.status !== 0) {
|
|
434
|
+
const detail = String(result.stderr || result.stdout || "").trim();
|
|
435
|
+
const suffix = detail ? `: ${detail.slice(-4000)}` : "";
|
|
436
|
+
throw new Error(
|
|
437
|
+
`secure filesystem ${operation} failed for ${targetPath}${suffix}`
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
return String(result.stdout || "").trim();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function privateFileStat(filePath) {
|
|
444
|
+
let stat;
|
|
445
|
+
try {
|
|
446
|
+
stat = fs.lstatSync(filePath);
|
|
447
|
+
} catch (error) {
|
|
448
|
+
if (error.code === "ENOENT") {
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
453
|
+
if (stat.isSymbolicLink()) {
|
|
454
|
+
throw new Error(`refusing symbolic link in managed path: ${filePath}`);
|
|
455
|
+
}
|
|
456
|
+
if (!stat.isFile()) {
|
|
457
|
+
throw new Error(`managed path is not a file: ${filePath}`);
|
|
458
|
+
}
|
|
459
|
+
return stat;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function validatePrivateDirectory(directory) {
|
|
463
|
+
runSecureFilesystemOperation("validate-directory", path.resolve(directory));
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function createTempRuntime(finalDirectory) {
|
|
467
|
+
return runSecureFilesystemOperation("create-temp-directory", path.resolve(finalDirectory));
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function removeTempRuntime(directory) {
|
|
471
|
+
runSecureFilesystemOperation("remove-tree", path.resolve(directory));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function cleanupStaleTempRuntimes(parentDirectory, staleMs = TEMP_RUNTIME_STALE_MS) {
|
|
475
|
+
runSecureFilesystemOperation(
|
|
476
|
+
"cleanup-temp-directories",
|
|
477
|
+
path.resolve(parentDirectory),
|
|
478
|
+
"",
|
|
479
|
+
[String(Date.now() - staleMs)]
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function publishRuntime(tempDirectory, finalDirectory, platformOverride = "") {
|
|
484
|
+
return runSecureFilesystemOperation(
|
|
485
|
+
"publish-directory",
|
|
486
|
+
path.resolve(tempDirectory),
|
|
487
|
+
"",
|
|
488
|
+
[path.resolve(finalDirectory), platformOverride]
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function candidatePythons() {
|
|
493
|
+
const configured = process.env.KAGENT_PYTHON ? [process.env.KAGENT_PYTHON] : [];
|
|
494
|
+
return configured.concat(["python3", "python"]);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function commandWorks(command, args) {
|
|
498
|
+
const result = childProcess.spawnSync(command, args, {
|
|
499
|
+
encoding: "utf8",
|
|
500
|
+
stdio: "ignore"
|
|
501
|
+
});
|
|
502
|
+
return result.status === 0;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function findPython() {
|
|
506
|
+
for (const command of candidatePythons()) {
|
|
507
|
+
if (commandWorks(command, ["-c", "import sys; raise SystemExit(0 if sys.version_info >= (3, 9) else 1)"])) {
|
|
508
|
+
return command;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
throw new Error("kagent requires Python 3.9+. Install python3 or set KAGENT_PYTHON.");
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function runChecked(command, args, options) {
|
|
515
|
+
const result = childProcess.spawnSync(command, args, {
|
|
516
|
+
cwd: options.cwd,
|
|
517
|
+
env: process.env,
|
|
518
|
+
encoding: "utf8",
|
|
519
|
+
stdio: options.stdio || "inherit"
|
|
520
|
+
});
|
|
521
|
+
if (result.error) {
|
|
522
|
+
throw result.error;
|
|
523
|
+
}
|
|
524
|
+
if (result.status !== 0) {
|
|
525
|
+
const detail = [result.stdout, result.stderr]
|
|
526
|
+
.filter((value) => typeof value === "string" && value.trim())
|
|
527
|
+
.join("\n")
|
|
528
|
+
.trim();
|
|
529
|
+
const suffix = detail ? `\n${detail.slice(-4000)}` : "";
|
|
530
|
+
throw new Error(
|
|
531
|
+
`${command} ${args.join(" ")} failed with exit code ${result.status}${suffix}`
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function readUpdateState(env = process.env) {
|
|
537
|
+
const statePath = updateStatePath(env);
|
|
538
|
+
rejectSymlinks(statePath);
|
|
539
|
+
if (!privateFileStat(statePath)) {
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
try {
|
|
543
|
+
const state = JSON.parse(fs.readFileSync(statePath, "utf8"));
|
|
544
|
+
return state && typeof state === "object" ? state : null;
|
|
545
|
+
} catch (_error) {
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function writeUpdateState(state, env = process.env) {
|
|
551
|
+
const statePath = path.join(ensureMetadataCacheRoot(env), "npm-self-update.json");
|
|
552
|
+
writePrivateFile(statePath, `${JSON.stringify(state, null, 2)}\n`);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function venvPythonPath(venvDir) {
|
|
556
|
+
if (process.platform === "win32") {
|
|
557
|
+
return path.join(venvDir, "Scripts", "python.exe");
|
|
558
|
+
}
|
|
559
|
+
return path.join(venvDir, "bin", "python");
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function markerPath(venvDir) {
|
|
563
|
+
return path.join(venvDir, ".kagent-node-install.json");
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function installMarker(version, dependencyFingerprint, pythonIdentityHash) {
|
|
567
|
+
return {
|
|
568
|
+
schema: 1,
|
|
569
|
+
dependencyHash: dependencyFingerprint,
|
|
570
|
+
pythonIdentityHash,
|
|
571
|
+
createdFromVersion: version
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function sha256(value) {
|
|
576
|
+
return crypto.createHash("sha256").update(value).digest("hex");
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function projectTable(pyproject) {
|
|
580
|
+
const match = pyproject.match(/^\s*\[project\]\s*(?:#.*)?$/m);
|
|
581
|
+
if (!match) {
|
|
582
|
+
throw new Error("pyproject.toml does not declare [project]");
|
|
583
|
+
}
|
|
584
|
+
const bodyStart = match.index + match[0].length;
|
|
585
|
+
const remaining = pyproject.slice(bodyStart);
|
|
586
|
+
const nextTable = remaining.search(/^\s*\[/m);
|
|
587
|
+
return nextTable === -1 ? remaining : remaining.slice(0, nextTable);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function tomlString(value) {
|
|
591
|
+
if (value.startsWith('"')) {
|
|
592
|
+
return JSON.parse(value);
|
|
593
|
+
}
|
|
594
|
+
return value.slice(1, -1);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function projectString(project, key) {
|
|
598
|
+
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
599
|
+
const match = project.match(
|
|
600
|
+
new RegExp(`^\\s*${escapedKey}\\s*=\\s*("(?:\\\\.|[^"\\\\])*"|'[^']*')\\s*(?:#.*)?$`, "m")
|
|
601
|
+
);
|
|
602
|
+
if (!match) {
|
|
603
|
+
throw new Error(`pyproject.toml [project] does not declare ${key}`);
|
|
604
|
+
}
|
|
605
|
+
return tomlString(match[1]);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function projectStringArray(project, key) {
|
|
609
|
+
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
610
|
+
const assignment = new RegExp(`^\\s*${escapedKey}\\s*=\\s*\\[`, "m").exec(project);
|
|
611
|
+
if (!assignment) {
|
|
612
|
+
throw new Error(`pyproject.toml [project] does not declare ${key}`);
|
|
613
|
+
}
|
|
614
|
+
const values = [];
|
|
615
|
+
let index = assignment.index + assignment[0].length;
|
|
616
|
+
while (index < project.length) {
|
|
617
|
+
const character = project[index];
|
|
618
|
+
if (character === "]") {
|
|
619
|
+
return values;
|
|
620
|
+
}
|
|
621
|
+
if (character === "#") {
|
|
622
|
+
while (index < project.length && project[index] !== "\n") index += 1;
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
if (character === '"' || character === "'") {
|
|
626
|
+
const quote = character;
|
|
627
|
+
const start = index;
|
|
628
|
+
index += 1;
|
|
629
|
+
while (index < project.length) {
|
|
630
|
+
if (quote === '"' && project[index] === "\\") {
|
|
631
|
+
index += 2;
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
if (project[index] === quote) {
|
|
635
|
+
index += 1;
|
|
636
|
+
values.push(tomlString(project.slice(start, index)));
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
index += 1;
|
|
640
|
+
}
|
|
641
|
+
if (project[index - 1] !== quote) {
|
|
642
|
+
throw new Error(`unterminated string in [project].${key}`);
|
|
643
|
+
}
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
index += 1;
|
|
647
|
+
}
|
|
648
|
+
throw new Error(`unterminated array in [project].${key}`);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function dependencyHash(root) {
|
|
652
|
+
const pyproject = fs.readFileSync(path.join(root, "pyproject.toml"), "utf8");
|
|
653
|
+
const project = projectTable(pyproject);
|
|
654
|
+
return sha256(JSON.stringify({
|
|
655
|
+
requiresPython: projectString(project, "requires-python"),
|
|
656
|
+
dependencies: projectStringArray(project, "dependencies")
|
|
657
|
+
}));
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function readMarker(venvDir) {
|
|
661
|
+
const pathToMarker = markerPath(venvDir);
|
|
662
|
+
if (!privateFileStat(pathToMarker)) {
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
try {
|
|
666
|
+
return JSON.parse(fs.readFileSync(pathToMarker, "utf8"));
|
|
667
|
+
} catch (_error) {
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function markerMatches(actual, expected) {
|
|
673
|
+
return Boolean(actual) &&
|
|
674
|
+
actual.schema === expected.schema &&
|
|
675
|
+
actual.dependencyHash === expected.dependencyHash &&
|
|
676
|
+
actual.pythonIdentityHash === expected.pythonIdentityHash;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function writeMarker(venvDir, marker) {
|
|
680
|
+
writePrivateFile(markerPath(venvDir), `${JSON.stringify(marker, null, 2)}\n`);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function pythonRuntimeIdentity(python) {
|
|
684
|
+
const result = childProcess.spawnSync(
|
|
685
|
+
python,
|
|
686
|
+
["-c", "import json, os, platform, sys, sysconfig; print(json.dumps({'implementation': sys.implementation.name, 'major': sys.version_info[0], 'minor': sys.version_info[1], 'cacheTag': sys.implementation.cache_tag, 'soabi': sysconfig.get_config_var('SOABI'), 'machine': platform.machine(), 'executable': os.path.realpath(sys.executable), 'prefix': os.path.realpath(sys.prefix), 'basePrefix': os.path.realpath(sys.base_prefix), 'execPrefix': os.path.realpath(sys.exec_prefix), 'baseExecPrefix': os.path.realpath(sys.base_exec_prefix)}))"],
|
|
687
|
+
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }
|
|
688
|
+
);
|
|
689
|
+
if (result.error) {
|
|
690
|
+
throw result.error;
|
|
691
|
+
}
|
|
692
|
+
if (result.status !== 0) {
|
|
693
|
+
throw new Error(`failed to identify Python runtime: ${String(result.stderr || "").trim()}`);
|
|
694
|
+
}
|
|
695
|
+
const identity = JSON.parse(result.stdout);
|
|
696
|
+
if (!/^[A-Za-z0-9_-]+$/.test(identity.implementation) ||
|
|
697
|
+
identity.implementation.length > 24 ||
|
|
698
|
+
!Number.isInteger(identity.major) || !Number.isInteger(identity.minor) ||
|
|
699
|
+
identity.major < 0 || identity.minor < 0) {
|
|
700
|
+
throw new Error("Python runtime returned an invalid identity");
|
|
701
|
+
}
|
|
702
|
+
return identity;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function safeRuntimeComponent(value, label, maxLength) {
|
|
706
|
+
if (typeof value !== "string" || value.length === 0 || value.length > maxLength ||
|
|
707
|
+
!/^[A-Za-z0-9_.-]+$/.test(value)) {
|
|
708
|
+
throw new Error(`invalid ${label} runtime component`);
|
|
709
|
+
}
|
|
710
|
+
return value;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function runtimeCacheDirectory(cache, identity, platform, arch, dependencyFingerprint) {
|
|
714
|
+
const implementation = safeRuntimeComponent(identity.implementation, "implementation", 24);
|
|
715
|
+
const hostPlatform = safeRuntimeComponent(platform, "platform", 24);
|
|
716
|
+
const hostArch = safeRuntimeComponent(arch, "architecture", 32);
|
|
717
|
+
const identityFingerprint = sha256(JSON.stringify(identity));
|
|
718
|
+
const abi = `${implementation}-${identity.major}.${identity.minor}-${identityFingerprint}`;
|
|
719
|
+
return path.join(cache, abi, `${hostPlatform}-${hostArch}`, dependencyFingerprint);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function runtimeState(venvDir, expectedMarker, pythonWorks = commandWorks) {
|
|
723
|
+
let stat;
|
|
724
|
+
try {
|
|
725
|
+
stat = fs.lstatSync(venvDir);
|
|
726
|
+
} catch (error) {
|
|
727
|
+
if (error.code === "ENOENT") {
|
|
728
|
+
return "missing";
|
|
729
|
+
}
|
|
730
|
+
throw error;
|
|
731
|
+
}
|
|
732
|
+
if (stat.isSymbolicLink()) {
|
|
733
|
+
throw new Error(`refusing symbolic link in managed path: ${venvDir}`);
|
|
734
|
+
}
|
|
735
|
+
if (!stat.isDirectory()) {
|
|
736
|
+
throw new Error(`invalid cached Python runtime: ${venvDir}`);
|
|
737
|
+
}
|
|
738
|
+
validatePrivateDirectory(venvDir);
|
|
739
|
+
const marker = readMarker(venvDir);
|
|
740
|
+
const pythonPath = venvPythonPath(venvDir);
|
|
741
|
+
if (!markerMatches(marker, expectedMarker) ||
|
|
742
|
+
!fs.existsSync(pythonPath) || !pythonWorks(pythonPath, ["-c", "import kagent"])) {
|
|
743
|
+
throw new Error(`invalid cached Python runtime: ${venvDir}`);
|
|
744
|
+
}
|
|
745
|
+
return "valid";
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function ensureVenv(root, version, options = {}) {
|
|
749
|
+
const platform = options.platform || process.platform;
|
|
750
|
+
assertSupportedPlatform(platform);
|
|
751
|
+
const python = options.python || findPython();
|
|
752
|
+
const identity = options.pythonIdentity || pythonRuntimeIdentity(python);
|
|
753
|
+
const dependencyFingerprint = dependencyHash(root);
|
|
754
|
+
const pythonIdentityHash = sha256(JSON.stringify(identity));
|
|
755
|
+
const cache = options.cacheRoot || ensureCacheRoot();
|
|
756
|
+
const arch = options.arch || process.arch;
|
|
757
|
+
const venvDir = runtimeCacheDirectory(cache, identity, platform, arch, dependencyFingerprint);
|
|
758
|
+
const ensureDirectory = options.ensurePrivateDirectory || ensurePrivateDirectory;
|
|
759
|
+
const checkedRun = options.runChecked || runChecked;
|
|
760
|
+
const markerWriter = options.writeMarker || writeMarker;
|
|
761
|
+
const pythonWorks = options.runtimePythonWorks || commandWorks;
|
|
762
|
+
const expectedMarker = installMarker(version, dependencyFingerprint, pythonIdentityHash);
|
|
763
|
+
ensureDirectory(path.dirname(venvDir));
|
|
764
|
+
if (runtimeState(venvDir, expectedMarker, pythonWorks) === "valid") {
|
|
765
|
+
return venvDir;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
cleanupStaleTempRuntimes(path.dirname(venvDir), options.tempRuntimeStaleMs);
|
|
769
|
+
if (runtimeState(venvDir, expectedMarker, pythonWorks) === "valid") {
|
|
770
|
+
return venvDir;
|
|
771
|
+
}
|
|
772
|
+
const tempDir = createTempRuntime(venvDir);
|
|
773
|
+
try {
|
|
774
|
+
process.stderr.write(`kagent: preparing Python runtime in ${tempDir}\n`);
|
|
775
|
+
checkedRun(python, ["-m", "venv", tempDir], { cwd: root });
|
|
776
|
+
const tempPython = venvPythonPath(tempDir);
|
|
777
|
+
process.stderr.write("kagent: preparing Python runtime\n");
|
|
778
|
+
checkedRun(
|
|
779
|
+
tempPython,
|
|
780
|
+
["-m", "pip", "install", "--disable-pip-version-check", "--quiet", root],
|
|
781
|
+
{ cwd: root, stdio: "pipe" }
|
|
782
|
+
);
|
|
783
|
+
markerWriter(tempDir, expectedMarker);
|
|
784
|
+
if (runtimeState(tempDir, expectedMarker, pythonWorks) !== "valid") {
|
|
785
|
+
throw new Error(`failed to prepare Python runtime: ${tempDir}`);
|
|
786
|
+
}
|
|
787
|
+
const publishResult = publishRuntime(tempDir, venvDir);
|
|
788
|
+
if (publishResult !== "published" && publishResult !== "exists") {
|
|
789
|
+
throw new Error(`invalid Python runtime publish result: ${publishResult}`);
|
|
790
|
+
}
|
|
791
|
+
if (runtimeState(venvDir, expectedMarker, pythonWorks) !== "valid") {
|
|
792
|
+
throw new Error(`invalid cached Python runtime: ${venvDir}`);
|
|
793
|
+
}
|
|
794
|
+
return venvDir;
|
|
795
|
+
} finally {
|
|
796
|
+
removeTempRuntime(tempDir);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function pythonEnvironment(root, env = process.env) {
|
|
801
|
+
const source = path.join(root, "src");
|
|
802
|
+
return Object.assign({}, env, {
|
|
803
|
+
PYTHONPATH: env.PYTHONPATH ? `${source}${path.delimiter}${env.PYTHONPATH}` : source
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function pythonEntrypointArgs(commandName, args) {
|
|
808
|
+
const code = PYTHON_ENTRYPOINTS[commandName];
|
|
809
|
+
if (!code) {
|
|
810
|
+
throw new Error(`unsupported Python entrypoint: ${commandName}`);
|
|
811
|
+
}
|
|
812
|
+
return ["-c", code, commandName].concat(args || []);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function spawnEntrypoint(venvDir, root, commandName, args) {
|
|
816
|
+
const command = venvPythonPath(venvDir);
|
|
817
|
+
const result = childProcess.spawnSync(command, pythonEntrypointArgs(commandName, args), {
|
|
818
|
+
env: pythonEnvironment(root),
|
|
819
|
+
stdio: "inherit"
|
|
820
|
+
});
|
|
821
|
+
if (result.error) {
|
|
822
|
+
throw result.error;
|
|
823
|
+
}
|
|
824
|
+
process.exit(result.status === null ? 1 : result.status);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function ensurePythonRuntime() {
|
|
828
|
+
const root = packageRoot();
|
|
829
|
+
const version = readPackageVersion(root);
|
|
830
|
+
const venvDir = ensureVenv(root, version);
|
|
831
|
+
return {
|
|
832
|
+
root,
|
|
833
|
+
version,
|
|
834
|
+
venvDir,
|
|
835
|
+
pythonPath: venvPythonPath(venvDir)
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function spawnPythonModule(moduleName, args, options) {
|
|
840
|
+
const runtime = ensurePythonRuntime();
|
|
841
|
+
const provided = options || {};
|
|
842
|
+
const spawnOptions = Object.assign(
|
|
843
|
+
{ cwd: runtime.root, stdio: ["pipe", "pipe", "pipe"] },
|
|
844
|
+
provided
|
|
845
|
+
);
|
|
846
|
+
spawnOptions.env = pythonEnvironment(
|
|
847
|
+
runtime.root,
|
|
848
|
+
Object.assign({}, process.env, provided.env || {})
|
|
849
|
+
);
|
|
850
|
+
return childProcess.spawn(
|
|
851
|
+
runtime.pythonPath,
|
|
852
|
+
["-m", moduleName].concat(args || []),
|
|
853
|
+
spawnOptions
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function runPythonEntrypoint(commandName, args) {
|
|
858
|
+
try {
|
|
859
|
+
const root = packageRoot();
|
|
860
|
+
const version = readPackageVersion(root);
|
|
861
|
+
if (maybePrintNodeHandledOutput(commandName, args, version)) {
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
const venvDir = ensureVenv(root, version);
|
|
865
|
+
spawnEntrypoint(venvDir, root, commandName, args);
|
|
866
|
+
} catch (error) {
|
|
867
|
+
process.stderr.write(`kagent failed to start: ${error.message}\n`);
|
|
868
|
+
process.exit(1);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
module.exports = {
|
|
873
|
+
ensurePythonRuntime,
|
|
874
|
+
runPythonEntrypoint,
|
|
875
|
+
spawnPythonModule,
|
|
876
|
+
_internals: {
|
|
877
|
+
cacheRoot,
|
|
878
|
+
dependencyHash,
|
|
879
|
+
ensureCacheRoot,
|
|
880
|
+
ensurePrivateDirectory,
|
|
881
|
+
ensureVenv,
|
|
882
|
+
metadataCacheRoot,
|
|
883
|
+
maybePrintNodeHandledOutput,
|
|
884
|
+
pythonEnvironment,
|
|
885
|
+
pythonEntrypointArgs,
|
|
886
|
+
pythonRuntimeIdentity,
|
|
887
|
+
publishRuntime,
|
|
888
|
+
readUpdateState,
|
|
889
|
+
spawnEntrypoint,
|
|
890
|
+
writeUpdateState
|
|
891
|
+
}
|
|
892
|
+
};
|