wafer-cli 0.2.19__tar.gz → 0.2.21__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.
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/PKG-INFO +1 -1
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/pyproject.toml +1 -1
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/tests/test_cli_coverage.py +14 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/tests/test_wevin_cli.py +86 -7
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer/cli.py +58 -20
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer/templates/optimize_kernel.py +2 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer/wevin_cli.py +45 -17
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer_cli.egg-info/PKG-INFO +1 -1
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/README.md +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/setup.cfg +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/tests/test_analytics.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/tests/test_auth.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/tests/test_billing.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/tests/test_cli_parity_integration.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/tests/test_config_integration.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/tests/test_file_operations_integration.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/tests/test_kernel_scope_cli.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/tests/test_nsys_analyze.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/tests/test_nsys_profile.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/tests/test_output.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/tests/test_rocprof_compute_integration.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/tests/test_skill_commands.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/tests/test_ssh_integration.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/tests/test_targets_ops.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/tests/test_workflow_integration.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer/GUIDE.md +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer/__init__.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer/analytics.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer/api_client.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer/auth.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer/autotuner.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer/billing.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer/config.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer/corpus.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer/evaluate.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer/global_config.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer/gpu_run.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer/inference.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer/kernel_scope.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer/ncu_analyze.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer/nsys_analyze.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer/nsys_profile.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer/output.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer/problems.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer/rocprof_compute.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer/rocprof_sdk.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer/rocprof_systems.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer/skills/wafer-guide/SKILL.md +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer/ssh_keys.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer/target_lock.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer/targets.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer/targets_ops.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer/templates/__init__.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer/templates/ask_docs.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer/templates/optimize_kernelbench.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer/templates/trace_analyze.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer/tracelens.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer/workspaces.py +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer_cli.egg-info/SOURCES.txt +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer_cli.egg-info/dependency_links.txt +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer_cli.egg-info/entry_points.txt +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer_cli.egg-info/requires.txt +0 -0
- {wafer_cli-0.2.19 → wafer_cli-0.2.21}/wafer_cli.egg-info/top_level.txt +0 -0
|
@@ -719,3 +719,17 @@ class TestWorkspacesExecFlagPassthrough:
|
|
|
719
719
|
"workspaces", "exec", "test-ws", "--", "cmd", "--output=/tmp/out"
|
|
720
720
|
])
|
|
721
721
|
assert "no such option" not in result.output.lower()
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
class TestAgentNoSandboxOption:
|
|
725
|
+
"""Test --no-sandbox option in wafer agent command."""
|
|
726
|
+
|
|
727
|
+
def test_agent_no_sandbox_option_exists(self) -> None:
|
|
728
|
+
"""Test that --no-sandbox option is accepted by wafer agent command."""
|
|
729
|
+
result = runner.invoke(app, ["agent", "--help"])
|
|
730
|
+
assert result.exit_code == 0
|
|
731
|
+
# Strip ANSI escape codes before checking (help output may contain color codes)
|
|
732
|
+
ansi_escape = re.compile(r'\x1b\[[0-9;]*m')
|
|
733
|
+
clean_output = ansi_escape.sub('', result.stdout)
|
|
734
|
+
assert "--no-sandbox" in clean_output
|
|
735
|
+
assert "liability" in clean_output.lower() # Warning text should be in help
|
|
@@ -634,35 +634,114 @@ def test_streaming_frontend_session_start_state_without_session_id():
|
|
|
634
634
|
|
|
635
635
|
def test_streaming_frontend_session_start_resumed_then_new():
|
|
636
636
|
"""Test session_start emission when resuming but states have different session_id.
|
|
637
|
-
|
|
637
|
+
|
|
638
638
|
Edge case: --resume used but states return different session_id (should use states one).
|
|
639
639
|
"""
|
|
640
640
|
import trio
|
|
641
641
|
|
|
642
642
|
from wafer.wevin_cli import StreamingChunkFrontend
|
|
643
|
-
|
|
643
|
+
|
|
644
644
|
async def run_test() -> None:
|
|
645
645
|
# Start with resumed session_id
|
|
646
646
|
frontend = StreamingChunkFrontend(
|
|
647
647
|
session_id="resumed-session-123",
|
|
648
648
|
model="claude-sonnet-4.5"
|
|
649
649
|
)
|
|
650
|
-
|
|
650
|
+
|
|
651
651
|
emitted_events = []
|
|
652
652
|
|
|
653
653
|
def mock_emit(obj) -> None:
|
|
654
654
|
emitted_events.append(obj)
|
|
655
|
-
|
|
655
|
+
|
|
656
656
|
frontend._emit = mock_emit
|
|
657
|
-
|
|
657
|
+
|
|
658
658
|
# start() emits session_start for resumed session
|
|
659
659
|
await frontend.start()
|
|
660
660
|
assert len(emitted_events) == 1
|
|
661
661
|
assert emitted_events[0]["session_id"] == "resumed-session-123"
|
|
662
|
-
|
|
662
|
+
|
|
663
663
|
# If states have different session_id (shouldn't happen, but handle gracefully)
|
|
664
664
|
# The logic in main() checks `if first_session_id and not session_id`
|
|
665
665
|
# So if session_id was set, it won't emit again
|
|
666
666
|
# This is correct behavior - use the one from --resume
|
|
667
|
-
|
|
667
|
+
|
|
668
668
|
trio.run(run_test)
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
# =============================================================================
|
|
672
|
+
# --no-sandbox flag tests
|
|
673
|
+
# =============================================================================
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def test_no_sandbox_parameter_accepted():
|
|
677
|
+
"""Test that no_sandbox parameter exists in wevin_main signature."""
|
|
678
|
+
import inspect
|
|
679
|
+
|
|
680
|
+
from wafer.wevin_cli import main as wevin_main
|
|
681
|
+
|
|
682
|
+
sig = inspect.signature(wevin_main)
|
|
683
|
+
params = sig.parameters
|
|
684
|
+
|
|
685
|
+
# Verify parameter exists
|
|
686
|
+
assert 'no_sandbox' in params
|
|
687
|
+
|
|
688
|
+
# Verify type and default
|
|
689
|
+
assert str(params['no_sandbox'].annotation) in ('bool', "<class 'bool'>")
|
|
690
|
+
assert params['no_sandbox'].default is False
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def test_build_environment_accepts_no_sandbox():
|
|
694
|
+
"""Test that _build_environment accepts no_sandbox parameter."""
|
|
695
|
+
import inspect
|
|
696
|
+
|
|
697
|
+
from wafer.wevin_cli import _build_environment
|
|
698
|
+
|
|
699
|
+
sig = inspect.signature(_build_environment)
|
|
700
|
+
params = sig.parameters
|
|
701
|
+
|
|
702
|
+
assert 'no_sandbox' in params
|
|
703
|
+
assert params['no_sandbox'].default is False
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
def test_build_environment_with_no_sandbox_false():
|
|
707
|
+
"""Test _build_environment creates env with sandbox ENABLED when no_sandbox=False."""
|
|
708
|
+
from wafer_core.rollouts.templates import TemplateConfig
|
|
709
|
+
from wafer_core.sandbox import SandboxMode
|
|
710
|
+
|
|
711
|
+
from wafer.wevin_cli import _build_environment
|
|
712
|
+
|
|
713
|
+
tpl = TemplateConfig(
|
|
714
|
+
name="test",
|
|
715
|
+
description="Test template",
|
|
716
|
+
system_prompt="Test",
|
|
717
|
+
tools=["read"],
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
# This will raise RuntimeError if sandbox is unavailable on this system
|
|
721
|
+
# That's expected - we're testing that sandbox is ENABLED by default
|
|
722
|
+
try:
|
|
723
|
+
env = _build_environment(tpl, None, None, no_sandbox=False)
|
|
724
|
+
# If we get here, sandbox is available - verify it's enabled
|
|
725
|
+
assert env.sandbox_mode == SandboxMode.ENABLED
|
|
726
|
+
except RuntimeError as e:
|
|
727
|
+
# Sandbox unavailable - that's OK, the error proves ENABLED is set
|
|
728
|
+
assert "sandboxing is not available" in str(e)
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def test_build_environment_with_no_sandbox_true():
|
|
732
|
+
"""Test _build_environment creates env with sandbox DISABLED when no_sandbox=True."""
|
|
733
|
+
from wafer_core.rollouts.templates import TemplateConfig
|
|
734
|
+
from wafer_core.sandbox import SandboxMode
|
|
735
|
+
|
|
736
|
+
from wafer.wevin_cli import _build_environment
|
|
737
|
+
|
|
738
|
+
tpl = TemplateConfig(
|
|
739
|
+
name="test",
|
|
740
|
+
description="Test template",
|
|
741
|
+
system_prompt="Test",
|
|
742
|
+
tools=["read"],
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
# This should NOT raise - sandbox is disabled
|
|
746
|
+
env = _build_environment(tpl, None, None, no_sandbox=True)
|
|
747
|
+
assert env.sandbox_mode == SandboxMode.DISABLED
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
# ruff: noqa: PLR0913
|
|
1
|
+
# ruff: noqa: PLR0913, E402
|
|
2
2
|
# PLR0913 (too many arguments) is suppressed because Typer CLI commands
|
|
3
3
|
# naturally have many parameters - each --flag becomes a function argument.
|
|
4
|
+
# E402 (module level import not at top) is suppressed because we intentionally
|
|
5
|
+
# load .env files before importing other modules that may read env vars.
|
|
4
6
|
"""Wafer CLI - GPU development toolkit for LLM coding agents.
|
|
5
7
|
|
|
6
8
|
Core commands:
|
|
@@ -27,6 +29,12 @@ from pathlib import Path
|
|
|
27
29
|
|
|
28
30
|
import trio
|
|
29
31
|
import typer
|
|
32
|
+
from dotenv import load_dotenv
|
|
33
|
+
|
|
34
|
+
# Auto-load .env from current directory and ~/.wafer/.env
|
|
35
|
+
# This runs at import time so env vars are available before any config is accessed
|
|
36
|
+
load_dotenv() # cwd/.env
|
|
37
|
+
load_dotenv(Path.home() / ".wafer" / ".env") # ~/.wafer/.env
|
|
30
38
|
|
|
31
39
|
from .config import WaferConfig, WaferEnvironment
|
|
32
40
|
from .inference import infer_upload_files, resolve_environment
|
|
@@ -42,6 +50,7 @@ from .problems import (
|
|
|
42
50
|
app = typer.Typer(
|
|
43
51
|
help="GPU development toolkit for LLM coding agents",
|
|
44
52
|
no_args_is_help=True,
|
|
53
|
+
pretty_exceptions_show_locals=False, # Don't dump local vars (makes tracebacks huge)
|
|
45
54
|
)
|
|
46
55
|
|
|
47
56
|
# =============================================================================
|
|
@@ -58,11 +67,11 @@ def _show_version() -> None:
|
|
|
58
67
|
"""Show CLI version and environment, then exit."""
|
|
59
68
|
from .analytics import _get_cli_version
|
|
60
69
|
from .global_config import load_global_config
|
|
61
|
-
|
|
70
|
+
|
|
62
71
|
version = _get_cli_version()
|
|
63
72
|
config = load_global_config()
|
|
64
73
|
environment = config.environment
|
|
65
|
-
|
|
74
|
+
|
|
66
75
|
typer.echo(f"wafer-cli {version} ({environment})")
|
|
67
76
|
raise typer.Exit()
|
|
68
77
|
|
|
@@ -110,7 +119,7 @@ def main_callback(
|
|
|
110
119
|
if version:
|
|
111
120
|
_show_version()
|
|
112
121
|
return
|
|
113
|
-
|
|
122
|
+
|
|
114
123
|
global _command_start_time, _command_outcome
|
|
115
124
|
_command_start_time = time.time()
|
|
116
125
|
_command_outcome = "success" # Default to success, mark failure on exceptions
|
|
@@ -121,6 +130,7 @@ def main_callback(
|
|
|
121
130
|
analytics.init_analytics()
|
|
122
131
|
|
|
123
132
|
# Install exception hook to catch SystemExit and mark failures
|
|
133
|
+
# Also prints error message FIRST so it's visible even when traceback is truncated
|
|
124
134
|
original_excepthook = sys.excepthook
|
|
125
135
|
|
|
126
136
|
def custom_excepthook(
|
|
@@ -136,7 +146,9 @@ def main_callback(
|
|
|
136
146
|
_command_outcome = "failure"
|
|
137
147
|
else:
|
|
138
148
|
_command_outcome = "failure"
|
|
139
|
-
|
|
149
|
+
# Print error summary FIRST (before traceback) so it's visible even if truncated
|
|
150
|
+
print(f"\n\033[1;31m>>> ERROR: {exc_type.__name__}: {exc_value}\033[0m\n", file=sys.stderr)
|
|
151
|
+
# Call original excepthook (prints the full traceback)
|
|
140
152
|
original_excepthook(exc_type, exc_value, exc_traceback)
|
|
141
153
|
|
|
142
154
|
sys.excepthook = custom_excepthook
|
|
@@ -591,7 +603,7 @@ app.add_typer(provider_auth_app, name="auth")
|
|
|
591
603
|
def provider_auth_login(
|
|
592
604
|
provider: str = typer.Argument(
|
|
593
605
|
...,
|
|
594
|
-
help="Provider name: runpod, digitalocean, or
|
|
606
|
+
help="Provider name: runpod, digitalocean, modal, anthropic, or openai",
|
|
595
607
|
),
|
|
596
608
|
api_key: str | None = typer.Option(
|
|
597
609
|
None,
|
|
@@ -600,15 +612,16 @@ def provider_auth_login(
|
|
|
600
612
|
help="API key (if not provided, reads from stdin)",
|
|
601
613
|
),
|
|
602
614
|
) -> None:
|
|
603
|
-
"""Save API key for a
|
|
615
|
+
"""Save API key for a provider.
|
|
604
616
|
|
|
605
617
|
Stores the key in ~/.wafer/auth.json. Environment variables
|
|
606
|
-
(e.g.,
|
|
618
|
+
(e.g., ANTHROPIC_API_KEY) take precedence over stored keys.
|
|
607
619
|
|
|
608
620
|
Examples:
|
|
621
|
+
wafer auth login anthropic --api-key sk-ant-xxx
|
|
609
622
|
wafer auth login runpod --api-key rp_xxx
|
|
610
|
-
wafer auth login
|
|
611
|
-
echo $API_KEY | wafer auth login
|
|
623
|
+
wafer auth login openai --api-key sk-xxx
|
|
624
|
+
echo $API_KEY | wafer auth login anthropic
|
|
612
625
|
"""
|
|
613
626
|
import sys
|
|
614
627
|
|
|
@@ -642,7 +655,7 @@ def provider_auth_login(
|
|
|
642
655
|
def provider_auth_logout(
|
|
643
656
|
provider: str = typer.Argument(
|
|
644
657
|
...,
|
|
645
|
-
help="Provider name: runpod, digitalocean, or
|
|
658
|
+
help="Provider name: runpod, digitalocean, modal, anthropic, or openai",
|
|
646
659
|
),
|
|
647
660
|
) -> None:
|
|
648
661
|
"""Remove stored API key for a cloud GPU provider.
|
|
@@ -1327,6 +1340,11 @@ def agent( # noqa: PLR0913
|
|
|
1327
1340
|
"-c",
|
|
1328
1341
|
help="Documentation corpus to use (cuda, cutlass, hip, amd). Must be downloaded first.",
|
|
1329
1342
|
),
|
|
1343
|
+
no_sandbox: bool = typer.Option(
|
|
1344
|
+
False,
|
|
1345
|
+
"--no-sandbox",
|
|
1346
|
+
help="Disable OS-level sandboxing (YOU accept liability for any damage caused by the agent)",
|
|
1347
|
+
),
|
|
1330
1348
|
) -> None:
|
|
1331
1349
|
"""AI assistant for GPU kernel development.
|
|
1332
1350
|
|
|
@@ -1408,6 +1426,13 @@ def agent( # noqa: PLR0913
|
|
|
1408
1426
|
raise typer.Exit(1) from None
|
|
1409
1427
|
corpus_path = str(path)
|
|
1410
1428
|
|
|
1429
|
+
# Warn user about sandbox disabled
|
|
1430
|
+
if no_sandbox:
|
|
1431
|
+
print(
|
|
1432
|
+
"Warning: Sandbox disabled. You accept liability for any damage caused by the agent.",
|
|
1433
|
+
file=sys.stderr,
|
|
1434
|
+
)
|
|
1435
|
+
|
|
1411
1436
|
wevin_main(
|
|
1412
1437
|
prompt=actual_prompt,
|
|
1413
1438
|
interactive=use_tui,
|
|
@@ -1425,6 +1450,7 @@ def agent( # noqa: PLR0913
|
|
|
1425
1450
|
template=template,
|
|
1426
1451
|
template_args=parsed_template_args,
|
|
1427
1452
|
corpus_path=corpus_path,
|
|
1453
|
+
no_sandbox=no_sandbox,
|
|
1428
1454
|
)
|
|
1429
1455
|
|
|
1430
1456
|
|
|
@@ -1455,6 +1481,7 @@ def _make_agent_alias(name: str, doc: str) -> None:
|
|
|
1455
1481
|
template: str | None = typer.Option(None, "--template", "-t"),
|
|
1456
1482
|
template_args: list[str] | None = typer.Option(None, "--args"),
|
|
1457
1483
|
corpus: str | None = typer.Option(None, "--corpus"),
|
|
1484
|
+
no_sandbox: bool = typer.Option(False, "--no-sandbox"),
|
|
1458
1485
|
) -> None:
|
|
1459
1486
|
agent(
|
|
1460
1487
|
prompt=prompt,
|
|
@@ -1474,6 +1501,7 @@ def _make_agent_alias(name: str, doc: str) -> None:
|
|
|
1474
1501
|
template=template,
|
|
1475
1502
|
template_args=template_args,
|
|
1476
1503
|
corpus=corpus,
|
|
1504
|
+
no_sandbox=no_sandbox,
|
|
1477
1505
|
)
|
|
1478
1506
|
|
|
1479
1507
|
alias_cmd.__doc__ = doc
|
|
@@ -4391,9 +4419,13 @@ def workspaces_list(
|
|
|
4391
4419
|
@workspaces_app.command("create")
|
|
4392
4420
|
def workspaces_create(
|
|
4393
4421
|
name: str = typer.Argument(..., help="Workspace name"),
|
|
4394
|
-
gpu_type: str = typer.Option(
|
|
4422
|
+
gpu_type: str = typer.Option(
|
|
4423
|
+
"B200", "--gpu", "-g", help="GPU type: MI300X (AMD) or B200 (NVIDIA, default)"
|
|
4424
|
+
),
|
|
4395
4425
|
image: str | None = typer.Option(None, "--image", "-i", help="Docker image (optional)"),
|
|
4396
|
-
wait: bool = typer.Option(
|
|
4426
|
+
wait: bool = typer.Option(
|
|
4427
|
+
False, "--wait", "-w", help="Wait for provisioning and show SSH credentials"
|
|
4428
|
+
),
|
|
4397
4429
|
json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
|
|
4398
4430
|
) -> None:
|
|
4399
4431
|
"""Create a new workspace.
|
|
@@ -4702,19 +4734,25 @@ def workspaces_ssh(
|
|
|
4702
4734
|
ssh_host = ws.get("ssh_host")
|
|
4703
4735
|
ssh_port = ws.get("ssh_port")
|
|
4704
4736
|
ssh_user = ws.get("ssh_user")
|
|
4705
|
-
|
|
4737
|
+
|
|
4706
4738
|
if not ssh_host or not ssh_port or not ssh_user:
|
|
4707
4739
|
typer.echo("Error: Workspace not ready. Wait a few seconds and retry.", err=True)
|
|
4708
4740
|
raise typer.Exit(1)
|
|
4709
4741
|
|
|
4710
4742
|
# Connect via SSH
|
|
4711
|
-
os.execvp(
|
|
4743
|
+
os.execvp(
|
|
4712
4744
|
"ssh",
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4745
|
+
[
|
|
4746
|
+
"ssh",
|
|
4747
|
+
"-p",
|
|
4748
|
+
str(ssh_port),
|
|
4749
|
+
"-o",
|
|
4750
|
+
"StrictHostKeyChecking=no",
|
|
4751
|
+
"-o",
|
|
4752
|
+
"UserKnownHostsFile=/dev/null",
|
|
4753
|
+
f"{ssh_user}@{ssh_host}",
|
|
4754
|
+
],
|
|
4755
|
+
)
|
|
4718
4756
|
|
|
4719
4757
|
|
|
4720
4758
|
@workspaces_app.command("sync")
|
|
@@ -266,18 +266,27 @@ def _build_environment(
|
|
|
266
266
|
tpl: TemplateConfig,
|
|
267
267
|
tools_override: list[str] | None,
|
|
268
268
|
corpus_path: str | None,
|
|
269
|
+
no_sandbox: bool = False,
|
|
269
270
|
) -> Environment:
|
|
270
271
|
"""Build a CodingEnvironment from template config."""
|
|
271
272
|
from wafer_core.environments.coding import CodingEnvironment
|
|
272
273
|
from wafer_core.rollouts.templates import DANGEROUS_BASH_COMMANDS
|
|
274
|
+
from wafer_core.sandbox import SandboxMode
|
|
273
275
|
|
|
274
276
|
working_dir = Path(corpus_path) if corpus_path else Path.cwd()
|
|
275
|
-
resolved_tools = tools_override or tpl.tools
|
|
277
|
+
resolved_tools = list(tools_override or tpl.tools)
|
|
278
|
+
|
|
279
|
+
# Add skill tool if skills are enabled
|
|
280
|
+
if tpl.include_skills and "skill" not in resolved_tools:
|
|
281
|
+
resolved_tools.append("skill")
|
|
282
|
+
|
|
283
|
+
sandbox_mode = SandboxMode.DISABLED if no_sandbox else SandboxMode.ENABLED
|
|
276
284
|
env: Environment = CodingEnvironment(
|
|
277
285
|
working_dir=working_dir,
|
|
278
286
|
enabled_tools=resolved_tools,
|
|
279
287
|
bash_allowlist=tpl.bash_allowlist,
|
|
280
288
|
bash_denylist=DANGEROUS_BASH_COMMANDS,
|
|
289
|
+
sandbox_mode=sandbox_mode,
|
|
281
290
|
) # type: ignore[assignment]
|
|
282
291
|
return env
|
|
283
292
|
|
|
@@ -362,6 +371,7 @@ def main( # noqa: PLR0913, PLR0915
|
|
|
362
371
|
list_sessions: bool = False,
|
|
363
372
|
get_session: str | None = None,
|
|
364
373
|
json_output: bool = False,
|
|
374
|
+
no_sandbox: bool = False,
|
|
365
375
|
) -> None:
|
|
366
376
|
"""Run wevin agent in-process via rollouts."""
|
|
367
377
|
from dataclasses import asdict
|
|
@@ -373,6 +383,7 @@ def main( # noqa: PLR0913, PLR0915
|
|
|
373
383
|
|
|
374
384
|
# Handle --get-session: load session by ID and print
|
|
375
385
|
if get_session:
|
|
386
|
+
|
|
376
387
|
async def _get_session() -> None:
|
|
377
388
|
try:
|
|
378
389
|
session, err = await session_store.get(get_session)
|
|
@@ -393,16 +404,18 @@ def main( # noqa: PLR0913, PLR0915
|
|
|
393
404
|
error_msg = f"Failed to serialize messages: {e}"
|
|
394
405
|
print(json.dumps({"error": error_msg}))
|
|
395
406
|
sys.exit(1)
|
|
396
|
-
|
|
397
|
-
print(
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
407
|
+
|
|
408
|
+
print(
|
|
409
|
+
json.dumps({
|
|
410
|
+
"session_id": session.session_id,
|
|
411
|
+
"status": session.status.value,
|
|
412
|
+
"model": session.endpoint.model if session.endpoint else None,
|
|
413
|
+
"created_at": session.created_at,
|
|
414
|
+
"updated_at": session.updated_at,
|
|
415
|
+
"messages": messages_data,
|
|
416
|
+
"tags": session.tags,
|
|
417
|
+
})
|
|
418
|
+
)
|
|
406
419
|
else:
|
|
407
420
|
print(f"Session: {session.session_id}")
|
|
408
421
|
print(f"Status: {session.status.value}")
|
|
@@ -490,7 +503,7 @@ def main( # noqa: PLR0913, PLR0915
|
|
|
490
503
|
print(f"Error loading template: {err}", file=sys.stderr)
|
|
491
504
|
sys.exit(1)
|
|
492
505
|
tpl = loaded_template
|
|
493
|
-
|
|
506
|
+
base_system_prompt = tpl.interpolate_prompt(template_args or {})
|
|
494
507
|
# Show template info when starting without a prompt
|
|
495
508
|
if not prompt and tpl.description:
|
|
496
509
|
print(f"Template: {tpl.name}", file=sys.stderr)
|
|
@@ -498,14 +511,27 @@ def main( # noqa: PLR0913, PLR0915
|
|
|
498
511
|
print(file=sys.stderr)
|
|
499
512
|
else:
|
|
500
513
|
tpl = _get_default_template()
|
|
501
|
-
|
|
514
|
+
base_system_prompt = tpl.system_prompt
|
|
515
|
+
|
|
516
|
+
# Append skill metadata if skills are enabled
|
|
517
|
+
if tpl.include_skills:
|
|
518
|
+
from wafer_core.rollouts.skills import discover_skills, format_skill_metadata_for_prompt
|
|
519
|
+
|
|
520
|
+
skill_metadata = discover_skills()
|
|
521
|
+
if skill_metadata:
|
|
522
|
+
skill_section = format_skill_metadata_for_prompt(skill_metadata)
|
|
523
|
+
system_prompt = base_system_prompt + "\n\n" + skill_section
|
|
524
|
+
else:
|
|
525
|
+
system_prompt = base_system_prompt
|
|
526
|
+
else:
|
|
527
|
+
system_prompt = base_system_prompt
|
|
502
528
|
|
|
503
529
|
# CLI args override template values
|
|
504
530
|
resolved_single_turn = single_turn if single_turn is not None else tpl.single_turn
|
|
505
531
|
|
|
506
532
|
# Build endpoint and environment
|
|
507
533
|
endpoint = _build_endpoint(tpl, model, api_base, api_key)
|
|
508
|
-
environment = _build_environment(tpl, tools, corpus_path)
|
|
534
|
+
environment = _build_environment(tpl, tools, corpus_path, no_sandbox)
|
|
509
535
|
|
|
510
536
|
# Session store
|
|
511
537
|
session_store = FileSessionStore()
|
|
@@ -545,7 +571,7 @@ def main( # noqa: PLR0913, PLR0915
|
|
|
545
571
|
else:
|
|
546
572
|
if json_output:
|
|
547
573
|
# Emit session_start if we have a session_id (from --resume)
|
|
548
|
-
model_name = endpoint.model if hasattr(endpoint,
|
|
574
|
+
model_name = endpoint.model if hasattr(endpoint, "model") else None
|
|
549
575
|
frontend = StreamingChunkFrontend(session_id=session_id, model=model_name)
|
|
550
576
|
else:
|
|
551
577
|
frontend = NoneFrontend(show_tool_calls=True, show_thinking=False)
|
|
@@ -560,9 +586,11 @@ def main( # noqa: PLR0913, PLR0915
|
|
|
560
586
|
# Emit session_start for new sessions (if session_id was None and we got one)
|
|
561
587
|
# Check first state to emit as early as possible
|
|
562
588
|
if json_output and isinstance(frontend, StreamingChunkFrontend):
|
|
563
|
-
first_session_id =
|
|
589
|
+
first_session_id = (
|
|
590
|
+
states[0].session_id if states and states[0].session_id else None
|
|
591
|
+
)
|
|
564
592
|
if first_session_id and not session_id: # New session created
|
|
565
|
-
model_name = endpoint.model if hasattr(endpoint,
|
|
593
|
+
model_name = endpoint.model if hasattr(endpoint, "model") else None
|
|
566
594
|
frontend.emit_session_start(first_session_id, model_name)
|
|
567
595
|
# Print resume command with full wafer agent prefix
|
|
568
596
|
if states and states[-1].session_id:
|
|
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
|