wafer-cli 0.2.38__tar.gz → 0.2.40__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.
Files changed (72) hide show
  1. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/PKG-INFO +1 -1
  2. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/pyproject.toml +1 -1
  3. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/agent_defaults.py +48 -1
  4. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/cli.py +84 -27
  5. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/wevin_cli.py +36 -3
  6. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/workspaces.py +8 -2
  7. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer_cli.egg-info/PKG-INFO +1 -1
  8. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/README.md +0 -0
  9. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/setup.cfg +0 -0
  10. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/tests/test_analytics.py +0 -0
  11. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/tests/test_auth.py +0 -0
  12. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/tests/test_billing.py +0 -0
  13. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/tests/test_cli_coverage.py +0 -0
  14. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/tests/test_cli_parity_integration.py +0 -0
  15. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/tests/test_config_integration.py +0 -0
  16. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/tests/test_file_operations_integration.py +0 -0
  17. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/tests/test_kernel_scope_cli.py +0 -0
  18. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/tests/test_nsys_analyze.py +0 -0
  19. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/tests/test_nsys_profile.py +0 -0
  20. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/tests/test_output.py +0 -0
  21. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/tests/test_rocprof_compute_integration.py +0 -0
  22. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/tests/test_skill_commands.py +0 -0
  23. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/tests/test_ssh_integration.py +0 -0
  24. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/tests/test_targets_ops.py +0 -0
  25. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/tests/test_wevin_cli.py +0 -0
  26. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/tests/test_workflow_integration.py +0 -0
  27. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/GUIDE.md +0 -0
  28. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/__init__.py +0 -0
  29. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/analytics.py +0 -0
  30. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/api_client.py +0 -0
  31. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/auth.py +0 -0
  32. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/autotuner.py +0 -0
  33. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/baseline.py +0 -0
  34. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/billing.py +0 -0
  35. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/cli_instructions.py +0 -0
  36. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/config.py +0 -0
  37. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/corpus.py +0 -0
  38. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/evaluate.py +0 -0
  39. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/global_config.py +0 -0
  40. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/gpu_run.py +0 -0
  41. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/inference.py +0 -0
  42. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/kernel_scope.py +0 -0
  43. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/ncu_analyze.py +0 -0
  44. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/nsys_analyze.py +0 -0
  45. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/nsys_profile.py +0 -0
  46. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/output.py +0 -0
  47. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/problems.py +0 -0
  48. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/rocprof_compute.py +0 -0
  49. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/rocprof_sdk.py +0 -0
  50. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/rocprof_systems.py +0 -0
  51. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/skills/wafer-guide/SKILL.md +0 -0
  52. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/specs_cli.py +0 -0
  53. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/ssh_keys.py +0 -0
  54. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/target_lock.py +0 -0
  55. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/targets.py +0 -0
  56. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/targets_cli.py +0 -0
  57. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/targets_ops.py +0 -0
  58. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/templates/__init__.py +0 -0
  59. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/templates/aiter_optimize.py +0 -0
  60. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/templates/ask_docs.py +0 -0
  61. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/templates/optimize_kernel.py +0 -0
  62. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/templates/optimize_kernelbench.py +0 -0
  63. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/templates/optimize_vllm.py +0 -0
  64. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/templates/trace_analyze.py +0 -0
  65. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/tests/test_eval_cli_parity.py +0 -0
  66. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/trace_compare.py +0 -0
  67. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer/tracelens.py +0 -0
  68. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer_cli.egg-info/SOURCES.txt +0 -0
  69. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer_cli.egg-info/dependency_links.txt +0 -0
  70. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer_cli.egg-info/entry_points.txt +0 -0
  71. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer_cli.egg-info/requires.txt +0 -0
  72. {wafer_cli-0.2.38 → wafer_cli-0.2.40}/wafer_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wafer-cli
3
- Version: 0.2.38
3
+ Version: 0.2.40
4
4
  Summary: CLI for running GPU workloads, managing remote workspaces, and evaluating/optimizing kernels
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "wafer-cli"
3
- version = "0.2.38"
3
+ version = "0.2.40"
4
4
  description = "CLI for running GPU workloads, managing remote workspaces, and evaluating/optimizing kernels"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,4 +1,4 @@
1
- """Shared agent defaults for kernel optimization tasks.
1
+ """Shared agent defaults for kernel tasks.
2
2
 
3
3
  Single source of truth for bash allowlists and enabled tools used by both:
4
4
  - CLI templates (apps/wafer-cli/wafer/templates/*.py)
@@ -195,3 +195,50 @@ VLLM_BASH_ALLOWLIST: list[str] = [
195
195
  "cd",
196
196
  "git",
197
197
  ]
198
+
199
+ # Tools available to audit agents (read-only + bash for compilation/profiling)
200
+ AUDIT_ENABLED_TOOLS: list[str] = ["read", "glob", "grep", "bash"]
201
+
202
+ # Bash commands allowed for kernel audit agents.
203
+ AUDIT_BASH_ALLOWLIST: list[str] = [
204
+ # Read-only
205
+ "ls",
206
+ "cat",
207
+ "head",
208
+ "tail",
209
+ "wc",
210
+ "find",
211
+ "grep",
212
+ "rg",
213
+ "pwd",
214
+ "tree",
215
+ "which",
216
+ "diff",
217
+ "sort",
218
+ # Filesystem
219
+ "mkdir",
220
+ # Compilation
221
+ "make",
222
+ "cmake",
223
+ "nvcc",
224
+ "hipcc",
225
+ "g++",
226
+ "gcc",
227
+ "clang",
228
+ "python",
229
+ "python3",
230
+ # Execution — allows running compiled binaries via ./path.
231
+ # Security note: the agent can already compile arbitrary code via hipcc/gcc/etc,
232
+ # so blocking ./ execution doesn't add meaningful protection.
233
+ "./",
234
+ # Profiling
235
+ "wafer evaluate",
236
+ "wafer nvidia ncu",
237
+ "wafer nvidia nsys",
238
+ "wafer amd rocprof-compute",
239
+ "wafer amd rocprof-sdk",
240
+ "wafer amd rocprof-systems",
241
+ "wafer compiler-analyze",
242
+ # Misc
243
+ "timeout",
244
+ ]
@@ -220,16 +220,22 @@ workspaces_app = typer.Typer(
220
220
 
221
221
  Workspaces are on-demand cloud GPU environments. Requires authentication (wafer login).
222
222
 
223
+ Environment Types:
224
+ modal Serverless GPU execution (fast startup, no SSH)
225
+ baremetal Dedicated GPU server (SSH access, hardware counters)
226
+
223
227
  Available GPUs:
224
- MI300X AMD Instinct MI300X (192GB HBM3, ROCm)
228
+ MI300X AMD Instinct MI300X (192GB HBM3, ROCm) - baremetal only
225
229
  B200 NVIDIA Blackwell B200 (180GB HBM3e, CUDA)
230
+ H100 NVIDIA Hopper H100 (80GB HBM3, CUDA)
226
231
 
227
232
  Commands:
228
- wafer workspaces create dev --gpu B200 # Create workspace
229
- wafer workspaces exec dev -- python x.py # Run commands
230
- wafer workspaces ssh dev # Interactive SSH
231
- wafer workspaces sync dev ./project # Sync files
232
- wafer workspaces delete dev # Clean up"""
233
+ wafer workspaces create dev -g B200 -e modal # Create Modal workspace
234
+ wafer workspaces create dev -g B200 -e baremetal # Create baremetal workspace
235
+ wafer workspaces exec dev -- python x.py # Run commands
236
+ wafer workspaces ssh dev # Interactive SSH (baremetal only)
237
+ wafer workspaces sync dev ./project # Sync files
238
+ wafer workspaces delete dev # Clean up"""
233
239
  )
234
240
  app.add_typer(workspaces_app, name="workspaces")
235
241
 
@@ -1862,6 +1868,12 @@ def kernelbench_evaluate( # noqa: PLR0913, PLR0915
1862
1868
  help="Sync files and generate eval script but don't run. "
1863
1869
  "Prints the command to run manually (useful for wrapping with rocprof, etc.)",
1864
1870
  ),
1871
+ pool_timeout: int = typer.Option(
1872
+ 600,
1873
+ "--pool-timeout",
1874
+ help="Seconds to wait for a target from the pool before failing (default: 600). "
1875
+ "Set to 0 for immediate failure if all targets are busy.",
1876
+ ),
1865
1877
  json_output: bool = typer.Option(
1866
1878
  False, "--json", help="Output as single JSON object (machine-readable)"
1867
1879
  ),
@@ -1953,14 +1965,24 @@ def kernelbench_evaluate( # noqa: PLR0913, PLR0915
1953
1965
  collector.finalize()
1954
1966
  raise typer.Exit(1) from None
1955
1967
 
1956
- collector.emit("pool_acquire", pool=pool, count=len(usable_targets))
1957
- pool_lock_context = acquire_from_pool(usable_targets)
1968
+ effective_timeout = pool_timeout if pool_timeout > 0 else None
1969
+ collector.emit("pool_acquire", pool=pool, count=len(usable_targets), timeout=pool_timeout)
1970
+ pool_lock_context = acquire_from_pool(usable_targets, timeout=effective_timeout)
1958
1971
  acquired_target = pool_lock_context.__enter__()
1959
1972
 
1960
1973
  if acquired_target is None:
1961
1974
  # Exit context manager before raising to avoid resource leak
1962
1975
  pool_lock_context.__exit__(None, None, None)
1963
- collector.set_error("pool", "AllTargetsBusy", pool=pool, targets=usable_targets)
1976
+ if pool_timeout > 0:
1977
+ collector.set_error(
1978
+ "pool",
1979
+ "AllTargetsBusy",
1980
+ pool=pool,
1981
+ targets=usable_targets,
1982
+ message=f"All targets busy after waiting {pool_timeout}s",
1983
+ )
1984
+ else:
1985
+ collector.set_error("pool", "AllTargetsBusy", pool=pool, targets=usable_targets)
1964
1986
  collector.finalize()
1965
1987
  raise typer.Exit(1)
1966
1988
 
@@ -2315,6 +2337,12 @@ def gpumode_evaluate( # noqa: PLR0913, PLR0915
2315
2337
  True, "--sync-artifacts/--no-sync-artifacts", help="Download artifacts"
2316
2338
  ),
2317
2339
  gpu_id: int | None = typer.Option(None, "--gpu-id", help="Override GPU ID"),
2340
+ pool_timeout: int = typer.Option(
2341
+ 600,
2342
+ "--pool-timeout",
2343
+ help="Seconds to wait for a target from the pool before failing (default: 600). "
2344
+ "Set to 0 for immediate failure if all targets are busy.",
2345
+ ),
2318
2346
  ) -> None:
2319
2347
  """Run kernel evaluation in GPUMode format (functional).
2320
2348
 
@@ -2394,14 +2422,21 @@ def gpumode_evaluate( # noqa: PLR0913, PLR0915
2394
2422
  typer.echo(" Run 'wafer auth status' to see which providers need setup.", err=True)
2395
2423
  raise typer.Exit(1) from None
2396
2424
 
2425
+ effective_timeout = pool_timeout if pool_timeout > 0 else None
2397
2426
  typer.echo(f"Acquiring target from pool '{pool}' ({len(usable_targets)} targets)...")
2398
- pool_lock_context = acquire_from_pool(usable_targets)
2427
+ pool_lock_context = acquire_from_pool(usable_targets, timeout=effective_timeout)
2399
2428
  acquired_target = pool_lock_context.__enter__()
2400
2429
 
2401
2430
  if acquired_target is None:
2402
2431
  # Exit context manager before raising to avoid resource leak
2403
2432
  pool_lock_context.__exit__(None, None, None)
2404
- typer.echo(f"Error: All targets in pool '{pool}' are busy", err=True)
2433
+ if pool_timeout > 0:
2434
+ typer.echo(
2435
+ f"Error: All targets in pool '{pool}' are busy (waited {pool_timeout}s)",
2436
+ err=True,
2437
+ )
2438
+ else:
2439
+ typer.echo(f"Error: All targets in pool '{pool}' are busy", err=True)
2405
2440
  typer.echo(f" Targets: {', '.join(usable_targets)}", err=True)
2406
2441
  raise typer.Exit(1)
2407
2442
 
@@ -3243,7 +3278,7 @@ def demo_eval(
3243
3278
  # Step 1: Create workspace
3244
3279
  typer.echo(f"\n[1/4] Creating workspace '{workspace_name}'...")
3245
3280
  result = subprocess.run(
3246
- ["wafer", "workspaces", "create", workspace_name, "--gpu", "B200", "--json"],
3281
+ ["wafer", "workspaces", "create", workspace_name, "--gpu", "B200", "--environment", "modal", "--json"],
3247
3282
  capture_output=True,
3248
3283
  text=True,
3249
3284
  check=True,
@@ -3330,7 +3365,7 @@ print(f"Performance: {(t1-t0)/100*1e6:.1f} us/iter")
3330
3365
  typer.echo("\n✓ Demo complete! To evaluate your own kernels:")
3331
3366
  typer.echo("")
3332
3367
  typer.echo(" # Using workspaces (no setup required):")
3333
- typer.echo(" wafer workspaces create dev --gpu B200")
3368
+ typer.echo(" wafer workspaces create dev --gpu B200 --environment modal")
3334
3369
  typer.echo(" wafer workspaces exec --sync ./my-kernel dev -- python my_test.py")
3335
3370
  typer.echo("")
3336
3371
  typer.echo(" # Or using wafer evaluate with a configured target:")
@@ -4787,7 +4822,10 @@ def workspaces_list(
4787
4822
  def workspaces_create(
4788
4823
  name: str = typer.Argument(..., help="Workspace name"),
4789
4824
  gpu_type: str = typer.Option(
4790
- "B200", "--gpu", "-g", help="GPU type: MI300X (AMD) or B200 (NVIDIA, default)"
4825
+ ..., "--gpu", "-g", help="GPU type: MI300X (AMD) or B200/H100 (NVIDIA)"
4826
+ ),
4827
+ environment: str = typer.Option(
4828
+ ..., "--environment", "-e", help="Environment type: modal or baremetal"
4791
4829
  ),
4792
4830
  image: str | None = typer.Option(None, "--image", "-i", help="Docker image (optional)"),
4793
4831
  wait: bool = typer.Option(
@@ -4797,16 +4835,22 @@ def workspaces_create(
4797
4835
  ) -> None:
4798
4836
  """Create a new workspace.
4799
4837
 
4838
+ Per-vendor architecture: each workspace has a single environment type.
4839
+
4840
+ Environment Types:
4841
+ modal Serverless GPU execution (fast startup, no SSH)
4842
+ baremetal Dedicated GPU server (SSH access, hardware counters)
4843
+
4800
4844
  Available GPUs:
4801
- MI300X AMD Instinct MI300X (192GB HBM3, ROCm)
4802
- B200 NVIDIA Blackwell B200 (180GB HBM3e, CUDA)
4845
+ MI300X AMD Instinct MI300X (192GB HBM3, ROCm) - baremetal only
4846
+ B200 NVIDIA Blackwell B200 (180GB HBM3e, CUDA)
4847
+ H100 NVIDIA Hopper H100 (80GB HBM3, CUDA)
4803
4848
 
4804
4849
  Example:
4805
- wafer workspaces create my-kernel # B200 (default)
4806
- wafer workspaces create my-kernel --gpu MI300X # AMD MI300X
4807
- wafer workspaces create my-kernel --gpu B200 # NVIDIA B200
4808
- wafer workspaces create my-kernel --image pytorch/pytorch:2.5.1-cuda12.4-cudnn9-devel
4809
- wafer workspaces create my-kernel --wait
4850
+ wafer workspaces create my-kernel --gpu B200 --environment modal
4851
+ wafer workspaces create my-kernel --gpu MI300X --environment baremetal
4852
+ wafer workspaces create my-kernel -g B200 -e baremetal # SSH + ncu profiling
4853
+ wafer workspaces create my-kernel -g B200 -e modal --wait
4810
4854
  """
4811
4855
  from .workspaces import create_workspace
4812
4856
 
@@ -4814,6 +4858,7 @@ def workspaces_create(
4814
4858
  result = create_workspace(
4815
4859
  name,
4816
4860
  gpu_type=gpu_type,
4861
+ environment_type=environment,
4817
4862
  image=image,
4818
4863
  wait=wait,
4819
4864
  json_output=json_output,
@@ -7751,16 +7796,24 @@ def compare_analyze(
7751
7796
  "-f",
7752
7797
  help="Output format: text, text-layers, csv, csv-layers, json",
7753
7798
  ),
7754
- output: Path | None = typer.Option(None, "--output", "-o", help="Output file (default: stdout)"),
7799
+ output: Path | None = typer.Option(
7800
+ None, "--output", "-o", help="Output file (default: stdout)"
7801
+ ),
7755
7802
  phase: str = typer.Option(
7756
7803
  "all",
7757
7804
  "--phase",
7758
7805
  help="Filter by phase: all, prefill, decode",
7759
7806
  ),
7760
7807
  layers: bool = typer.Option(False, "--layers", help="Show layer-wise performance breakdown"),
7761
- all: bool = typer.Option(False, "--all", help="Show all items (no truncation for layers, operations, kernels)"),
7762
- stack_traces: bool = typer.Option(False, "--stack-traces", help="Show Python stack traces for operations"),
7763
- json: bool = typer.Option(False, "--json", hidden=True, help="Ignored (for compatibility with cliExecutor)"),
7808
+ all: bool = typer.Option(
7809
+ False, "--all", help="Show all items (no truncation for layers, operations, kernels)"
7810
+ ),
7811
+ stack_traces: bool = typer.Option(
7812
+ False, "--stack-traces", help="Show Python stack traces for operations"
7813
+ ),
7814
+ json: bool = typer.Option(
7815
+ False, "--json", hidden=True, help="Ignored (for compatibility with cliExecutor)"
7816
+ ),
7764
7817
  ) -> None:
7765
7818
  """Compare GPU traces from two platforms platforms.
7766
7819
 
@@ -7824,13 +7877,17 @@ def compare_fusion_cmd(
7824
7877
  "-f",
7825
7878
  help="Output format: text, csv, json",
7826
7879
  ),
7827
- output: Path | None = typer.Option(None, "--output", "-o", help="Output file (default: stdout)"),
7880
+ output: Path | None = typer.Option(
7881
+ None, "--output", "-o", help="Output file (default: stdout)"
7882
+ ),
7828
7883
  min_group_size: int = typer.Option(
7829
7884
  50,
7830
7885
  "--min-group-size",
7831
7886
  help="Minimum correlation group size to analyze",
7832
7887
  ),
7833
- json: bool = typer.Option(False, "--json", hidden=True, help="Ignored (for compatibility with cliExecutor)"),
7888
+ json: bool = typer.Option(
7889
+ False, "--json", hidden=True, help="Ignored (for compatibility with cliExecutor)"
7890
+ ),
7834
7891
  ) -> None:
7835
7892
  """Analyze kernel fusion differences between AMD and NVIDIA traces.
7836
7893
 
@@ -326,13 +326,28 @@ def _build_environment(
326
326
  tools_override: list[str] | None,
327
327
  corpus_path: str | None,
328
328
  no_sandbox: bool = False,
329
+ has_target: bool = False,
330
+ template_args: dict[str, str] | None = None,
329
331
  ) -> Environment:
330
- """Build a CodingEnvironment from template config."""
332
+ """Build a CodingEnvironment from template config.
333
+
334
+ Working directory priority:
335
+ 1. Template arg "dir" (--args dir=./my_project) — scopes agent to a directory
336
+ 2. corpus_path (--corpus cuda) — for doc-browsing templates
337
+ 3. Current working directory
338
+ """
331
339
  from wafer_core.environments.coding import CodingEnvironment
332
340
  from wafer_core.rollouts.templates import DANGEROUS_BASH_COMMANDS
333
341
  from wafer_core.sandbox import SandboxMode
334
342
 
335
- working_dir = Path(corpus_path) if corpus_path else Path.cwd()
343
+ # Template arg "dir" takes priority over corpus_path
344
+ dir_arg = (template_args or {}).get("dir")
345
+ if dir_arg:
346
+ working_dir = Path(dir_arg).resolve()
347
+ elif corpus_path:
348
+ working_dir = Path(corpus_path)
349
+ else:
350
+ working_dir = Path.cwd()
336
351
  resolved_tools = list(tools_override or tpl.tools)
337
352
 
338
353
  # Add skill tool if skills are enabled
@@ -340,12 +355,18 @@ def _build_environment(
340
355
  resolved_tools.append("skill")
341
356
 
342
357
  sandbox_mode = SandboxMode.DISABLED if no_sandbox else SandboxMode.ENABLED
358
+
359
+ # Enable network when a target is configured — the agent needs to reach
360
+ # remote GPUs via SSH/HTTPS. Filesystem sandbox stays enforced.
361
+ allow_network = has_target
362
+
343
363
  env: Environment = CodingEnvironment(
344
364
  working_dir=working_dir,
345
365
  enabled_tools=resolved_tools,
346
366
  bash_allowlist=tpl.bash_allowlist,
347
367
  bash_denylist=DANGEROUS_BASH_COMMANDS,
348
368
  sandbox_mode=sandbox_mode,
369
+ allow_network=allow_network,
349
370
  ) # type: ignore[assignment]
350
371
  return env
351
372
 
@@ -589,9 +610,21 @@ def main( # noqa: PLR0913, PLR0915
589
610
  # CLI args override template values
590
611
  resolved_single_turn = single_turn if single_turn is not None else tpl.single_turn
591
612
 
613
+ # Check if a default target is configured — if so, enable network access
614
+ # so the agent can reach remote GPUs via SSH/HTTPS.
615
+ has_target = False
616
+ try:
617
+ from wafer.targets import get_default_target
618
+
619
+ has_target = get_default_target() is not None
620
+ except Exception:
621
+ pass # No target configured — network stays disabled
622
+
592
623
  # Build endpoint and environment
593
624
  endpoint = _build_endpoint(tpl, model, api_base, api_key, api_key_refresh)
594
- environment = _build_environment(tpl, tools, corpus_path, no_sandbox)
625
+ environment = _build_environment(
626
+ tpl, tools, corpus_path, no_sandbox, has_target=has_target, template_args=template_args
627
+ )
595
628
 
596
629
  # Session store
597
630
  session_store = FileSessionStore()
@@ -249,16 +249,20 @@ def list_workspaces(json_output: bool = False) -> str:
249
249
 
250
250
  def create_workspace(
251
251
  name: str,
252
- gpu_type: str = "B200",
252
+ gpu_type: str,
253
+ environment_type: str,
253
254
  image: str | None = None,
254
255
  wait: bool = False,
255
256
  json_output: bool = False,
256
257
  ) -> str:
257
258
  """Create a new workspace.
258
259
 
260
+ Per-vendor architecture: each workspace has a single environment type.
261
+
259
262
  Args:
260
263
  name: Workspace name (must be unique)
261
- gpu_type: GPU type (default: B200)
264
+ gpu_type: GPU type (required: B200, H100, MI300X)
265
+ environment_type: Environment type (required: modal, baremetal)
262
266
  image: Docker image (optional, uses default if not specified)
263
267
  wait: If True, stream provisioning progress and return SSH credentials
264
268
  json_output: If True, return raw JSON; otherwise return formatted text
@@ -272,6 +276,7 @@ def create_workspace(
272
276
  # Validate inputs
273
277
  assert name, "Workspace name must be non-empty"
274
278
  assert gpu_type, "GPU type must be non-empty"
279
+ assert environment_type, "Environment type must be non-empty"
275
280
 
276
281
  api_url, headers = _get_client()
277
282
 
@@ -296,6 +301,7 @@ def create_workspace(
296
301
  request_body: dict = {
297
302
  "name": name,
298
303
  "gpu_type": gpu_type,
304
+ "environment_type": environment_type,
299
305
  }
300
306
  if image:
301
307
  request_body["image"] = image
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wafer-cli
3
- Version: 0.2.38
3
+ Version: 0.2.40
4
4
  Summary: CLI for running GPU workloads, managing remote workspaces, and evaluating/optimizing kernels
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
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