kiwi-code 0.0.511__tar.gz → 0.0.512__tar.gz
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.
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/PKG-INFO +1 -1
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/pyproject.toml +1 -1
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_runtime/commands.py +95 -2
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_runtime/handlers.py +7 -2
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_runtime/link.py +15 -4
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_runtime/sessions.py +10 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_runtime/tui_bridge.py +22 -4
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_runtime_connection.py +157 -1
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/uv.lock +1 -1
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/.github/workflows/publish.yml +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/.github/workflows/test.yml +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/.gitignore +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/.python-version +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/Makefile +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/README.md +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/docs/runtime_audit.html +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/docs/social_login_e2e.md +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/scripts/set_edition.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/scripts/ws_auth_check.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/common/__init__.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/common/banner.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_cli/__init__.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_cli/_edition.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_cli/auth.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_cli/autocode.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_cli/checkpoints.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_cli/cli.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_cli/client.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_cli/commands.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_cli/edition.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_cli/human_questions.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_cli/io_threads.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_cli/logger.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_cli/models.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_cli/oauth.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_cli/run_activity.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_cli/runtime_manager.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_cli/server.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_cli/subagents.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_cli/terminal_mode.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_runtime/__init__.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_runtime/__main__.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_runtime/auth.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_runtime/checkpoints.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_runtime/cli.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_runtime/constants.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_runtime/fs/__init__.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_runtime/fs/batch.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_runtime/fs/codelens.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_runtime/fs/context.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_runtime/fs/edits.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_runtime/fs/handler.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_runtime/fs/paths.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_runtime/fs/read.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_runtime/fs/search.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_runtime/fs/text.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_runtime/fs/upload.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_runtime/fs/write.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_runtime/main.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_runtime/restricted.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_runtime/state.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_runtime/ui.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_runtime/uploads.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_tui/__init__.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_tui/attention.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_tui/autoupdate.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_tui/inline_file_picker.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_tui/latex_unicode.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_tui/main.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_tui/random_words.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_tui/role_mode.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_tui/runtime_agent.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_tui/screens/__init__.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_tui/screens/attach_content.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_tui/screens/command_result.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_tui/screens/detach_files.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_tui/screens/file_browser.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_tui/screens/help.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_tui/screens/id_picker.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_tui/screens/login.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_tui/screens/runtime_cleanup.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_tui/screens/runtime_logs.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_tui/screens/slash_picker.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_tui/screens/term_activity_view.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_tui/screens/term_dashboard.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_tui/screens/term_dashboard_css.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_tui/screens/term_dashboard_helpers.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_tui/screens/term_dashboard_widgets.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_tui/screens/term_question_widgets.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_tui/screens/term_status_line.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_tui/screens/term_subagent_widgets.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_tui/slash_commands.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_tui/status_words.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_tui/term_app.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_tui/terminal_theme.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_tui/ui_settings.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_tui/widgets.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/src/kiwi_tui/worktrees.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/test_hello.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/__init__.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/conftest.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_account_server.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_attention.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_autocode_actions.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_autoupdate.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_batch_fs_tool.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_checkpoints.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_cli_help.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_command_tips.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_copy_path_fs_tool.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_fs_edit_fidelity.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_human_questions.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_imports.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_io_threads.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_login_social.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_math_and_tables.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_oauth.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_read_file_streaming.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_reexec_kiwi.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_reply_selection.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_role_mode_injection.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_run_activity.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_run_steering.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_runtime_cleanup_per_account.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_runtime_edit_diffs.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_runtime_fixes.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_runtime_imports_on_windows.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_runtime_log_trimming.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_runtime_social_login.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_search_in_files_fs_tool.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_slash_commands.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_stats_command.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_status_line.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_subagents.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_term_dashboard_ui.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_terminal_mode.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_terminal_theme.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_toasts.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_tokens.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_tui_interactive_runtime.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_tui_palette.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_unified_diff.py +0 -0
- {kiwi_code-0.0.511 → kiwi_code-0.0.512}/tests/test_worktrees.py +0 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""One-off and interactive commands."""
|
|
2
2
|
import asyncio
|
|
3
|
+
import json
|
|
3
4
|
import os
|
|
5
|
+
import re
|
|
4
6
|
import shlex
|
|
5
7
|
import signal
|
|
6
8
|
import subprocess
|
|
@@ -9,6 +11,7 @@ import time
|
|
|
9
11
|
import uuid
|
|
10
12
|
from collections import deque
|
|
11
13
|
from typing import Any
|
|
14
|
+
from urllib.parse import unquote, urlsplit
|
|
12
15
|
|
|
13
16
|
from kiwi_runtime.constants import (
|
|
14
17
|
IS_WINDOWS,
|
|
@@ -222,13 +225,94 @@ async def _kill_command(proc: asyncio.subprocess.Process) -> None:
|
|
|
222
225
|
pass
|
|
223
226
|
|
|
224
227
|
|
|
228
|
+
#: Values shorter than this are not masked: too likely to match ordinary output.
|
|
229
|
+
_MIN_MASKED_LEN = 6
|
|
230
|
+
#: What a forwarded variable's name may be; anything else is refused.
|
|
231
|
+
_ENV_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
232
|
+
#: At most this many forwarded variables, each at most this long.
|
|
233
|
+
_MAX_ENV_VARS = 64
|
|
234
|
+
_MAX_ENV_VALUE = 16_384
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def command_env(value: Any) -> dict[str, str] | None:
|
|
238
|
+
"""
|
|
239
|
+
The `env` a one-off or interactive command message carries (connection credentials
|
|
240
|
+
the backend resolved for it), checked: string names a shell accepts, string values, a
|
|
241
|
+
bounded number. Anything else is dropped rather than half-applied. None when there is none.
|
|
242
|
+
"""
|
|
243
|
+
if not isinstance(value, dict) or not value:
|
|
244
|
+
return None
|
|
245
|
+
env: dict[str, str] = {}
|
|
246
|
+
for key, val in list(value.items())[:_MAX_ENV_VARS]:
|
|
247
|
+
if isinstance(key, str) and _ENV_NAME.match(key) and isinstance(val, str) and len(val) <= _MAX_ENV_VALUE:
|
|
248
|
+
env[key] = val
|
|
249
|
+
return env or None
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
#: Strings in a JSON credential (a service account's private key and the like) shorter
|
|
253
|
+
#: than this are not masked on their own: field names, types, project ids.
|
|
254
|
+
_MIN_JSON_SECRET_LEN = 16
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _secret_parts(value: str) -> list[str]:
|
|
258
|
+
"""
|
|
259
|
+
What of a forwarded value a command could print and must not: the value itself; for a
|
|
260
|
+
header like "Bearer <token>", the token; for a database connection string, its
|
|
261
|
+
password; for a JSON credential (BigQuery), its long string fields (a private key).
|
|
262
|
+
Kept in step with the backend's terminal_tool._secret_parts and the extension's
|
|
263
|
+
runtime/commandEnv.ts.
|
|
264
|
+
"""
|
|
265
|
+
parts = [value]
|
|
266
|
+
_, _, token = value.partition(" ")
|
|
267
|
+
parts.append(token)
|
|
268
|
+
if "://" in value:
|
|
269
|
+
try:
|
|
270
|
+
password = urlsplit(value).password
|
|
271
|
+
except ValueError:
|
|
272
|
+
password = None
|
|
273
|
+
if password:
|
|
274
|
+
parts += [password, unquote(password)]
|
|
275
|
+
if value.lstrip().startswith("{"):
|
|
276
|
+
try:
|
|
277
|
+
loaded = json.loads(value)
|
|
278
|
+
except ValueError:
|
|
279
|
+
loaded = None
|
|
280
|
+
if isinstance(loaded, dict):
|
|
281
|
+
for field in loaded.values():
|
|
282
|
+
if isinstance(field, str) and len(field) >= _MIN_JSON_SECRET_LEN:
|
|
283
|
+
parts += [field, field.replace("\n", "\\n")]
|
|
284
|
+
return parts
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def mask_secrets(text: str, env: dict[str, str] | None) -> str:
|
|
288
|
+
"""
|
|
289
|
+
Every forwarded value in `text` replaced by its variable's name, in case the command
|
|
290
|
+
printed one: each value whole, and the parts of it that are secrets on their own
|
|
291
|
+
(`_secret_parts`). (The backend masks again; this keeps it from leaving the machine
|
|
292
|
+
at all.)
|
|
293
|
+
"""
|
|
294
|
+
if not text or not env:
|
|
295
|
+
return text
|
|
296
|
+
pairs = [
|
|
297
|
+
(part, key)
|
|
298
|
+
for key, value in env.items()
|
|
299
|
+
for part in _secret_parts(value)
|
|
300
|
+
if len(part) >= _MIN_MASKED_LEN
|
|
301
|
+
]
|
|
302
|
+
for value, key in sorted(pairs, key=lambda p: len(p[0]), reverse=True):
|
|
303
|
+
text = text.replace(value, f"[{key}]")
|
|
304
|
+
return text
|
|
305
|
+
|
|
306
|
+
|
|
225
307
|
async def run_command(
|
|
226
308
|
command: str,
|
|
227
309
|
timeout: int = 30,
|
|
228
310
|
mode: str = "restricted",
|
|
229
311
|
allowed_dirs: list[str] | None = None,
|
|
312
|
+
env: dict[str, str] | None = None,
|
|
230
313
|
) -> dict:
|
|
231
|
-
"""Execute a shell command and return stdout, stderr, exit_code.
|
|
314
|
+
"""Execute a shell command and return stdout, stderr, exit_code. `env` is added to this
|
|
315
|
+
command's environment only, on top of the runtime's own; it is not logged."""
|
|
232
316
|
# Validate in restricted mode
|
|
233
317
|
if mode == "restricted" and allowed_dirs:
|
|
234
318
|
ok, reason = validate_command(command, allowed_dirs)
|
|
@@ -248,6 +332,8 @@ async def run_command(
|
|
|
248
332
|
"stdin": asyncio.subprocess.DEVNULL,
|
|
249
333
|
"cwd": cwd,
|
|
250
334
|
}
|
|
335
|
+
if env:
|
|
336
|
+
popen_kwargs["env"] = {**os.environ, **env}
|
|
251
337
|
if not IS_WINDOWS:
|
|
252
338
|
popen_kwargs["start_new_session"] = True
|
|
253
339
|
|
|
@@ -294,8 +380,11 @@ async def run_interactive_command(
|
|
|
294
380
|
mode: str = "restricted",
|
|
295
381
|
allowed_dirs: list[str] | None = None,
|
|
296
382
|
request_id: str | None = None,
|
|
383
|
+
env: dict[str, str] | None = None,
|
|
297
384
|
) -> dict:
|
|
298
|
-
"""Execute a command with the user's terminal attached for interactive I/O.
|
|
385
|
+
"""Execute a command with the user's terminal attached for interactive I/O. `env` is
|
|
386
|
+
added to this command's environment only, as for run_command. What the command prints
|
|
387
|
+
goes to the user's terminal, never to the server: the server gets only the exit code."""
|
|
299
388
|
if mode == "restricted" and allowed_dirs:
|
|
300
389
|
ok, reason = validate_command(command, allowed_dirs)
|
|
301
390
|
if not ok:
|
|
@@ -317,6 +406,8 @@ async def run_interactive_command(
|
|
|
317
406
|
mode=mode,
|
|
318
407
|
allowed_dirs=allowed_dirs,
|
|
319
408
|
timeout_sec=timeout_eff,
|
|
409
|
+
env=env,
|
|
410
|
+
mask=(lambda text: mask_secrets(text, env)) if env else None,
|
|
320
411
|
)
|
|
321
412
|
|
|
322
413
|
try:
|
|
@@ -346,6 +437,8 @@ async def run_interactive_command(
|
|
|
346
437
|
old_term = None
|
|
347
438
|
|
|
348
439
|
popen_kwargs: dict[str, Any] = {"shell": True, "cwd": cwd}
|
|
440
|
+
if env:
|
|
441
|
+
popen_kwargs["env"] = {**os.environ, **env}
|
|
349
442
|
if not IS_WINDOWS:
|
|
350
443
|
popen_kwargs["start_new_session"] = True
|
|
351
444
|
# The command writes to the terminal directly; what was printed goes first.
|
|
@@ -5,7 +5,7 @@ import sys
|
|
|
5
5
|
from typing import Any, Awaitable, Callable
|
|
6
6
|
|
|
7
7
|
from kiwi_runtime.checkpoints import _set_current_run_id
|
|
8
|
-
from kiwi_runtime.commands import _looks_interactive_oneoff, run_interactive_command
|
|
8
|
+
from kiwi_runtime.commands import _looks_interactive_oneoff, command_env, run_interactive_command
|
|
9
9
|
from kiwi_runtime.constants import IS_WINDOWS
|
|
10
10
|
from kiwi_runtime.fs.handler import FS_HANDLERS, handle_fs_request
|
|
11
11
|
from kiwi_runtime.restricted import validate_command
|
|
@@ -79,8 +79,10 @@ async def _command(state: _RuntimeState, msg: dict[str, Any]) -> None:
|
|
|
79
79
|
timeout_sec = int(msg.get("timeout_sec", 30) or 30)
|
|
80
80
|
except Exception:
|
|
81
81
|
timeout_sec = 30
|
|
82
|
+
# App connection credentials for this command only (the "command_env" capability).
|
|
83
|
+
env = command_env(msg.get("env"))
|
|
82
84
|
try:
|
|
83
|
-
state.command_queue.put_nowait((str(request_id), str(command), timeout_sec))
|
|
85
|
+
state.command_queue.put_nowait((str(request_id), str(command), timeout_sec, env))
|
|
84
86
|
except asyncio.QueueFull:
|
|
85
87
|
print_cmd_log(request_id, f"{RED}FAILED{RESET}", False)
|
|
86
88
|
await state.reply(_command_result(request_id, "Runtime command queue full; please retry."))
|
|
@@ -90,6 +92,8 @@ async def _interactive(state: _RuntimeState, msg: dict[str, Any]) -> None:
|
|
|
90
92
|
request_id = msg.get("request_id", "")
|
|
91
93
|
command = msg.get("command", "")
|
|
92
94
|
print_cmd_log(request_id, f"$ {BOLD}{command}{RESET} {GREY}(interactive){RESET}")
|
|
95
|
+
# Connection credentials for this command only; never logged.
|
|
96
|
+
env = command_env(msg.get("env"))
|
|
93
97
|
|
|
94
98
|
try:
|
|
95
99
|
timeout_sec = int(msg.get("timeout_sec", 120) or 120)
|
|
@@ -103,6 +107,7 @@ async def _interactive(state: _RuntimeState, msg: dict[str, Any]) -> None:
|
|
|
103
107
|
mode=state.mode,
|
|
104
108
|
allowed_dirs=state.allowed_dirs,
|
|
105
109
|
request_id=request_id,
|
|
110
|
+
env=env,
|
|
106
111
|
)
|
|
107
112
|
exit_code = result["exit_code"]
|
|
108
113
|
success = exit_code == 0
|
|
@@ -11,7 +11,7 @@ from pathlib import Path
|
|
|
11
11
|
import websockets
|
|
12
12
|
|
|
13
13
|
from kiwi_runtime.auth import _get_valid_access_token, login
|
|
14
|
-
from kiwi_runtime.commands import run_command
|
|
14
|
+
from kiwi_runtime.commands import mask_secrets, run_command
|
|
15
15
|
from kiwi_runtime.constants import MAX_INBOUND_FRAME_BYTES
|
|
16
16
|
from kiwi_runtime.fs.handler import FS_MAX_CONCURRENCY
|
|
17
17
|
from kiwi_runtime.handlers import handle_message
|
|
@@ -32,7 +32,10 @@ from kiwi_runtime.ui import (
|
|
|
32
32
|
# goodbye sends {"type": "goodbye"} before an intentional exit
|
|
33
33
|
# outbox keeps what it could not send and delivers it after reconnecting
|
|
34
34
|
# pty_survive keeps its PTY sessions across a reconnect and lists them at auth
|
|
35
|
-
|
|
35
|
+
# command_env takes an `env` mapping on a one-off or interactive command (connection
|
|
36
|
+
# credentials), adds it to that command's environment only, and masks it in
|
|
37
|
+
# the output
|
|
38
|
+
RUNTIME_CAPABILITIES = ["goodbye", "outbox", "pty_survive", "command_env"]
|
|
36
39
|
|
|
37
40
|
RECONNECT_BASE_DELAY_SEC = 1.5
|
|
38
41
|
RECONNECT_MAX_DELAY_SEC = 30.0
|
|
@@ -89,11 +92,11 @@ async def _command_worker(state: _RuntimeState, mode: str, allowed_dirs: list[st
|
|
|
89
92
|
"""Runs one-off commands from the queue; a result outlives the connection it was asked
|
|
90
93
|
on. Each command is its own shell, so several workers run them side by side."""
|
|
91
94
|
while True:
|
|
92
|
-
request_id, command, timeout_sec = await state.command_queue.get()
|
|
95
|
+
request_id, command, timeout_sec, env = await state.command_queue.get()
|
|
93
96
|
state.running_commands.append(command)
|
|
94
97
|
try:
|
|
95
98
|
result = await run_command(
|
|
96
|
-
command, timeout=timeout_sec or 30, mode=mode, allowed_dirs=allowed_dirs
|
|
99
|
+
command, timeout=timeout_sec or 30, mode=mode, allowed_dirs=allowed_dirs, env=env
|
|
97
100
|
)
|
|
98
101
|
try:
|
|
99
102
|
exit_code = int(result.get("exit_code", 0))
|
|
@@ -111,6 +114,14 @@ async def _command_worker(state: _RuntimeState, mode: str, allowed_dirs: list[st
|
|
|
111
114
|
finally:
|
|
112
115
|
state.running_commands.remove(command)
|
|
113
116
|
state.command_queue.task_done()
|
|
117
|
+
if env:
|
|
118
|
+
# A credential the command printed must not reach the server, the model or the
|
|
119
|
+
# chat: each value is replaced by its variable's name.
|
|
120
|
+
result = {
|
|
121
|
+
**result,
|
|
122
|
+
"stdout": mask_secrets(result.get("stdout", ""), env),
|
|
123
|
+
"stderr": mask_secrets(result.get("stderr", ""), env),
|
|
124
|
+
}
|
|
114
125
|
await state.link.send(json.dumps({"type": "result", "request_id": request_id, **result}))
|
|
115
126
|
|
|
116
127
|
|
|
@@ -149,9 +149,12 @@ if not IS_WINDOWS:
|
|
|
149
149
|
cols: int = 120,
|
|
150
150
|
rows: int = 40,
|
|
151
151
|
on_exit: Callable[[], None] | None = None,
|
|
152
|
+
env: dict[str, str] | None = None,
|
|
152
153
|
):
|
|
153
154
|
self.session_id = session_id
|
|
154
155
|
self.command = command
|
|
156
|
+
# Added to this process's environment only (connection credentials); not logged.
|
|
157
|
+
self.env = env
|
|
155
158
|
self.cols = cols
|
|
156
159
|
self.rows = rows
|
|
157
160
|
self.master_fd = None
|
|
@@ -190,6 +193,7 @@ if not IS_WINDOWS:
|
|
|
190
193
|
stderr=self.slave_fd,
|
|
191
194
|
preexec_fn=_own_terminal,
|
|
192
195
|
cwd=cwd,
|
|
196
|
+
env={**os.environ, **self.env} if self.env else None,
|
|
193
197
|
)
|
|
194
198
|
|
|
195
199
|
os.close(self.slave_fd)
|
|
@@ -422,9 +426,12 @@ class PipeProcess:
|
|
|
422
426
|
rows: int = 40,
|
|
423
427
|
mode: str = "restricted",
|
|
424
428
|
allowed_dirs: list[str] | None = None,
|
|
429
|
+
env: dict[str, str] | None = None,
|
|
425
430
|
):
|
|
426
431
|
self.session_id = session_id
|
|
427
432
|
self.command = command
|
|
433
|
+
# Added to this process's environment only (connection credentials); not logged.
|
|
434
|
+
self.env = env
|
|
428
435
|
self.cols = cols
|
|
429
436
|
self.rows = rows
|
|
430
437
|
self.proc = None
|
|
@@ -444,6 +451,7 @@ class PipeProcess:
|
|
|
444
451
|
self.cwd = cwd
|
|
445
452
|
self.ws = ws
|
|
446
453
|
self._sentinel_buffer = ""
|
|
454
|
+
env = {**os.environ, **self.env} if self.env else None
|
|
447
455
|
|
|
448
456
|
if IS_WINDOWS:
|
|
449
457
|
shell = os.environ.get("COMSPEC", "cmd.exe")
|
|
@@ -457,6 +465,7 @@ class PipeProcess:
|
|
|
457
465
|
stdout=asyncio.subprocess.PIPE,
|
|
458
466
|
stderr=asyncio.subprocess.STDOUT,
|
|
459
467
|
cwd=cwd,
|
|
468
|
+
env=env,
|
|
460
469
|
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP,
|
|
461
470
|
)
|
|
462
471
|
else:
|
|
@@ -467,6 +476,7 @@ class PipeProcess:
|
|
|
467
476
|
stdout=asyncio.subprocess.PIPE,
|
|
468
477
|
stderr=asyncio.subprocess.STDOUT,
|
|
469
478
|
cwd=cwd,
|
|
479
|
+
env=env,
|
|
470
480
|
)
|
|
471
481
|
|
|
472
482
|
self._read_task = asyncio.create_task(self._read_loop(ws))
|
|
@@ -5,7 +5,7 @@ import os
|
|
|
5
5
|
import uuid
|
|
6
6
|
from datetime import datetime
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import Any
|
|
8
|
+
from typing import Any, Callable
|
|
9
9
|
|
|
10
10
|
from kiwi_runtime.constants import (
|
|
11
11
|
IS_WINDOWS,
|
|
@@ -30,8 +30,17 @@ def _tui_managed_control_dir() -> Path | None:
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
class _LocalInteractivePTYBridge:
|
|
33
|
-
def __init__(
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
*,
|
|
36
|
+
control_dir: Path,
|
|
37
|
+
request_id: str,
|
|
38
|
+
command: str,
|
|
39
|
+
mask: Callable[[str], str] | None = None,
|
|
40
|
+
) -> None:
|
|
34
41
|
self.control_dir = control_dir
|
|
42
|
+
# Hides forwarded credentials in what is written to output.log for the TUI to show.
|
|
43
|
+
self._mask = mask
|
|
35
44
|
self.request_id = request_id
|
|
36
45
|
self.command = command
|
|
37
46
|
self.session_dir = control_dir / "interactive"
|
|
@@ -75,6 +84,10 @@ class _LocalInteractivePTYBridge:
|
|
|
75
84
|
def append_output(self, data: str) -> None:
|
|
76
85
|
if not data:
|
|
77
86
|
return
|
|
87
|
+
if self._mask is not None:
|
|
88
|
+
# Chunk by chunk: a value split across two reads is not caught. The file stays on
|
|
89
|
+
# this machine and nothing of it is sent to the server.
|
|
90
|
+
data = self._mask(data)
|
|
78
91
|
with open(self.output_path, "a", encoding="utf-8", errors="replace") as fp:
|
|
79
92
|
fp.write(data)
|
|
80
93
|
fp.flush()
|
|
@@ -179,6 +192,8 @@ async def run_tui_managed_interactive_command(
|
|
|
179
192
|
mode: str = "restricted",
|
|
180
193
|
allowed_dirs: list[str] | None = None,
|
|
181
194
|
timeout_sec: int | None = None,
|
|
195
|
+
env: dict[str, str] | None = None,
|
|
196
|
+
mask: Callable[[str], str] | None = None,
|
|
182
197
|
) -> dict:
|
|
183
198
|
if mode == "restricted" and allowed_dirs:
|
|
184
199
|
ok, reason = validate_command(command, allowed_dirs)
|
|
@@ -186,7 +201,9 @@ async def run_tui_managed_interactive_command(
|
|
|
186
201
|
return {"stdout": "", "stderr": reason, "exit_code": 1}
|
|
187
202
|
|
|
188
203
|
cwd = allowed_dirs[0] if allowed_dirs else None
|
|
189
|
-
bridge = _LocalInteractivePTYBridge(
|
|
204
|
+
bridge = _LocalInteractivePTYBridge(
|
|
205
|
+
control_dir=control_dir, request_id=request_id, command=command, mask=mask
|
|
206
|
+
)
|
|
190
207
|
await asyncio.to_thread(bridge.activate)
|
|
191
208
|
await asyncio.to_thread(bridge.append_output, f"$ {command}\r\n")
|
|
192
209
|
|
|
@@ -202,9 +219,10 @@ async def run_tui_managed_interactive_command(
|
|
|
202
219
|
40,
|
|
203
220
|
mode=mode,
|
|
204
221
|
allowed_dirs=allowed_dirs,
|
|
222
|
+
env=env,
|
|
205
223
|
)
|
|
206
224
|
else:
|
|
207
|
-
pty_proc = PTYProcess(session_id, command, 120, 40)
|
|
225
|
+
pty_proc = PTYProcess(session_id, command, 120, 40, env=env)
|
|
208
226
|
await pty_proc.start(bridge, cwd=cwd)
|
|
209
227
|
input_task = asyncio.create_task(bridge.pump_inputs(pty_proc))
|
|
210
228
|
|
|
@@ -313,7 +313,7 @@ async def test_it_states_what_it_can_do_when_it_authenticates(backend, run_runti
|
|
|
313
313
|
run_runtime()
|
|
314
314
|
auth = (await until(lambda: backend.auths))[0]
|
|
315
315
|
|
|
316
|
-
assert auth["capabilities"] == ["goodbye", "outbox", "pty_survive"]
|
|
316
|
+
assert auth["capabilities"] == ["goodbye", "outbox", "pty_survive", "command_env"]
|
|
317
317
|
assert auth["pty_sessions"] == []
|
|
318
318
|
assert auth["max_frame_bytes"] == runtime.MAX_INBOUND_FRAME_BYTES
|
|
319
319
|
|
|
@@ -642,6 +642,162 @@ async def test_one_off_commands_sent_together_run_side_by_side(backend, run_runt
|
|
|
642
642
|
assert {m["request_id"] for m in backend.messages("result")} == {f"r{n}" for n in range(runtime.COMMAND_WORKERS)}
|
|
643
643
|
|
|
644
644
|
|
|
645
|
+
# --- app connection credentials on a one-off command ("command_env") ----------------------
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
async def test_a_command_gets_the_variables_it_was_sent_and_its_output_is_masked(backend, run_runtime):
|
|
649
|
+
"""The backend resolved the user's saved connections into `env`: they reach that one
|
|
650
|
+
command, and a value it printed comes back as its variable's name."""
|
|
651
|
+
run_runtime()
|
|
652
|
+
await until(lambda: backend.sockets)
|
|
653
|
+
env = {
|
|
654
|
+
"AUTOBOTS_CONN_WORKDAY_PROD_AUTHORIZATION": "Bearer wd-secret-token-123",
|
|
655
|
+
"AWS_SECRET_ACCESS_KEY": "aws-secret-key-0001",
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
await backend.send({
|
|
659
|
+
"type": "command",
|
|
660
|
+
"request_id": "with-env",
|
|
661
|
+
"command": 'echo "$AUTOBOTS_CONN_WORKDAY_PROD_AUTHORIZATION"; echo "token=${AUTOBOTS_CONN_WORKDAY_PROD_AUTHORIZATION#Bearer }"; echo "$AWS_SECRET_ACCESS_KEY" >&2',
|
|
662
|
+
"timeout_sec": 10,
|
|
663
|
+
"env": env,
|
|
664
|
+
})
|
|
665
|
+
await until(lambda: backend.messages("result"))
|
|
666
|
+
result = backend.messages("result")[0]
|
|
667
|
+
|
|
668
|
+
assert result["exit_code"] == 0
|
|
669
|
+
assert result["stdout"] == "[AUTOBOTS_CONN_WORKDAY_PROD_AUTHORIZATION]\ntoken=[AUTOBOTS_CONN_WORKDAY_PROD_AUTHORIZATION]\n"
|
|
670
|
+
assert result["stderr"] == "[AWS_SECRET_ACCESS_KEY]\n"
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
async def test_the_variables_are_for_that_command_only(backend, run_runtime):
|
|
674
|
+
run_runtime()
|
|
675
|
+
await until(lambda: backend.sockets)
|
|
676
|
+
|
|
677
|
+
await backend.send({"type": "command", "request_id": "a", "command": "echo set", "timeout_sec": 10,
|
|
678
|
+
"env": {"AUTOBOTS_CONN_X_AUTHORIZATION": "secret-value-1"}})
|
|
679
|
+
await until(lambda: backend.messages("result"))
|
|
680
|
+
await backend.send({"type": "command", "request_id": "b", "command": 'echo "[${AUTOBOTS_CONN_X_AUTHORIZATION:-}]"', "timeout_sec": 10})
|
|
681
|
+
await until(lambda: len(backend.messages("result")) == 2)
|
|
682
|
+
|
|
683
|
+
later = next(m for m in backend.messages("result") if m["request_id"] == "b")
|
|
684
|
+
assert later["stdout"] == "[]\n"
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
@pytest.mark.skipif(sys.platform == "win32", reason="PTY")
|
|
688
|
+
async def test_an_interactive_command_under_the_tui_gets_the_variables_masked_on_screen(tmp_path, monkeypatch):
|
|
689
|
+
"""Under the TUI an interactive command runs on a PTY whose output the TUI reads from
|
|
690
|
+
output.log: the variables reach it, a printed value is masked there, and the server is
|
|
691
|
+
told only that it ended."""
|
|
692
|
+
from kiwi_runtime.commands import run_interactive_command
|
|
693
|
+
|
|
694
|
+
monkeypatch.setenv("KIWI_TUI_MANAGED", "1")
|
|
695
|
+
monkeypatch.setenv("KIWI_RUNTIME_CONTROL_DIR", str(tmp_path))
|
|
696
|
+
result = await run_interactive_command(
|
|
697
|
+
'test "$AUTOBOTS_CONN_GH_AUTHORIZATION" = "Bearer gh-secret-token-9" && echo "ok $AUTOBOTS_CONN_GH_AUTHORIZATION"',
|
|
698
|
+
timeout_sec=10,
|
|
699
|
+
mode="full",
|
|
700
|
+
request_id="i1",
|
|
701
|
+
env={"AUTOBOTS_CONN_GH_AUTHORIZATION": "Bearer gh-secret-token-9"},
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
shown = (tmp_path / "interactive" / "output.log").read_text()
|
|
705
|
+
assert result == {"stdout": "(interactive session completed)", "stderr": "", "exit_code": 0}
|
|
706
|
+
assert "ok [AUTOBOTS_CONN_GH_AUTHORIZATION]" in shown
|
|
707
|
+
assert "gh-secret-token-9" not in shown
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
async def test_an_interactive_command_in_a_plain_terminal_gets_the_variables(tmp_path, monkeypatch):
|
|
711
|
+
from kiwi_runtime.commands import run_interactive_command
|
|
712
|
+
|
|
713
|
+
monkeypatch.delenv("KIWI_TUI_MANAGED", raising=False)
|
|
714
|
+
out = tmp_path / "seen"
|
|
715
|
+
result = await run_interactive_command(
|
|
716
|
+
f'printf %s "$AUTOBOTS_DB_PG_URL" > "{out}"',
|
|
717
|
+
timeout_sec=10,
|
|
718
|
+
mode="full",
|
|
719
|
+
env={"AUTOBOTS_DB_PG_URL": "fake-connection-string-0001"},
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
assert result["exit_code"] == 0
|
|
723
|
+
assert out.read_text() == "fake-connection-string-0001"
|
|
724
|
+
assert "AUTOBOTS_DB_PG_URL" not in os.environ
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
async def test_an_interactive_message_carries_its_variables_to_the_command(monkeypatch):
|
|
728
|
+
seen: dict[str, Any] = {}
|
|
729
|
+
|
|
730
|
+
async def fake_run(command, **kwargs):
|
|
731
|
+
seen.update(kwargs)
|
|
732
|
+
return {"stdout": "(interactive session completed)", "stderr": "", "exit_code": 0}
|
|
733
|
+
|
|
734
|
+
monkeypatch.setattr(handlers, "run_interactive_command", fake_run)
|
|
735
|
+
replies: list[dict] = []
|
|
736
|
+
ran: list[Any] = []
|
|
737
|
+
|
|
738
|
+
class _Requests:
|
|
739
|
+
def submit(self, lane, job):
|
|
740
|
+
ran.append(job)
|
|
741
|
+
|
|
742
|
+
class _State:
|
|
743
|
+
mode = "full"
|
|
744
|
+
allowed_dirs: list[str] = []
|
|
745
|
+
requests = _Requests()
|
|
746
|
+
|
|
747
|
+
async def reply(self, msg):
|
|
748
|
+
replies.append(msg)
|
|
749
|
+
|
|
750
|
+
await handlers._interactive(_State(), {"type": "interactive", "request_id": "r", "command": "gh auth status",
|
|
751
|
+
"env": {"AUTOBOTS_CONN_GH_AUTHORIZATION": "Bearer t-123456", "1BAD": "x"}})
|
|
752
|
+
await ran[0]()
|
|
753
|
+
|
|
754
|
+
assert seen["env"] == {"AUTOBOTS_CONN_GH_AUTHORIZATION": "Bearer t-123456"}
|
|
755
|
+
assert replies[0]["exit_code"] == 0
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
def test_only_well_formed_variables_are_taken():
|
|
759
|
+
from kiwi_runtime.commands import command_env
|
|
760
|
+
|
|
761
|
+
assert command_env(None) is None
|
|
762
|
+
assert command_env({}) is None
|
|
763
|
+
assert command_env("A=B") is None
|
|
764
|
+
assert command_env({"GOOD_1": "v", "1BAD": "v", "BAD-NAME": "v", "NOT_TEXT": 3, "LONG": "x" * 20_000}) == {"GOOD_1": "v"}
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def test_a_printed_database_password_or_private_key_is_masked():
|
|
768
|
+
"""forward_databases_direct: a connection string's password and a BigQuery key, printed
|
|
769
|
+
on their own, are as secret as the whole value."""
|
|
770
|
+
import json as _json
|
|
771
|
+
|
|
772
|
+
from kiwi_runtime.commands import mask_secrets
|
|
773
|
+
|
|
774
|
+
# Made up, and put together from parts so secret scanners do not take them for real.
|
|
775
|
+
pem, body = "PRIVATE" + " KEY", "fake-key-body-for-tests-0001"
|
|
776
|
+
key = f"-----BEGIN {pem}-----\n{body}\n-----END {pem}-----\n"
|
|
777
|
+
word, encoded = "fake@word-99", "fake%40word-99"
|
|
778
|
+
env = {
|
|
779
|
+
"AUTOBOTS_DB_WAREHOUSE_URL": "postgresql://" + f"analyst:{encoded}@db.internal:5432/sales",
|
|
780
|
+
"AUTOBOTS_DB_BQ_CREDENTIALS_JSON": _json.dumps({"type": "service_account", "private_key": key}),
|
|
781
|
+
}
|
|
782
|
+
printed = f"pw={word} enc={encoded}\nraw: {key}json: {_json.dumps(key)}\n"
|
|
783
|
+
|
|
784
|
+
masked = mask_secrets(printed, env)
|
|
785
|
+
|
|
786
|
+
for secret in [word, encoded, body]:
|
|
787
|
+
assert secret not in masked
|
|
788
|
+
assert masked.startswith("pw=[AUTOBOTS_DB_WAREHOUSE_URL] enc=[AUTOBOTS_DB_WAREHOUSE_URL]\n")
|
|
789
|
+
assert "raw: [AUTOBOTS_DB_BQ_CREDENTIALS_JSON]" in masked
|
|
790
|
+
assert 'json: "[AUTOBOTS_DB_BQ_CREDENTIALS_JSON]"' in masked
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
def test_masking_leaves_short_values_and_ordinary_text_alone():
|
|
794
|
+
from kiwi_runtime.commands import mask_secrets
|
|
795
|
+
|
|
796
|
+
assert mask_secrets("value is abc", {"SHORT": "abc"}) == "value is abc"
|
|
797
|
+
assert mask_secrets("no secrets here", {"K": "long-secret-value"}) == "no secrets here"
|
|
798
|
+
assert mask_secrets("x long-secret-value y", None) == "x long-secret-value y"
|
|
799
|
+
|
|
800
|
+
|
|
645
801
|
# --- requests do not hold up the receive loop ---------------------------------------------
|
|
646
802
|
|
|
647
803
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|