pybun-cli 0.1.28__tar.gz → 0.2.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pybun-cli
3
- Version: 0.1.28
3
+ Version: 0.2.1
4
4
  Summary: PyBun shim bootstrapper for signed releases
5
5
  Author: PyBun Authors
6
6
  License-Expression: MIT
@@ -20,6 +20,13 @@ Dynamic: license-file
20
20
 
21
21
  # PyBun (Python Bundle)
22
22
 
23
+ # v0.2 backend transition
24
+
25
+ Normal project install/add/remove/lock/upgrade and project/PEP 723 execution now
26
+ delegate to uv. See [backend semantics and migration](docs/UV_BACKEND.md) for
27
+ the supported uv version, lock ownership, JSON v2, MCP parity, and explicit
28
+ native compatibility mode.
29
+
23
30
  <p align="center">
24
31
  <strong>🐍 The Agent-Native Python Control Plane 🤖</strong>
25
32
  </p>
@@ -264,6 +271,18 @@ pybun x cowsay==6.1
264
271
  pybun x black -- --check .
265
272
  ```
266
273
 
274
+ ### Affected Tests
275
+
276
+ ```bash
277
+ pybun test --affected
278
+ pybun test --affected --since main --format=json
279
+ pybun test --affected --discover --format=json
280
+ ```
281
+
282
+ `--changed` is an alias for `--affected`. Selection is repository-wide and cannot be combined with explicit paths or `--member`. MCP `pybun_test` uses the same service with `{"changed": true, "since": "main"}`.
283
+
284
+ JSON includes `selection` with `direct`, `transitive`, or `conservative` reasons and import chains. The target Python's standard AST parser reads project source without executing it. Imports and ancestor `conftest.py` dependencies are modeled; uncertain parsing, known dynamic imports, or non-Python/configuration changes conservatively select all tests. Import-root ambiguity may select extra tests. Arbitrary runtime file dependencies are not inferred. Unavailable git/runtime evidence returns an error rather than a verified empty selection.
285
+
267
286
  ### Python Version Management
268
287
 
269
288
  ```bash
@@ -361,6 +380,8 @@ pybun audit --severity-threshold=medium
361
380
  pybun audit --fail-on=high
362
381
  ```
363
382
 
383
+ The scanner follows OSV batch pagination and fetches advisory details before filtering or suggesting fixes. Missing severity is reported as `unknown`, remains visible at every threshold, and fails any `--fail-on` gate. Failed advisory lookups fail the scan. `PYBUN_OSV_URL` can point to a mirror's batch endpoint; its sibling `vulns/<id>` endpoint must serve full advisories.
384
+
364
385
  ### Profile Management
365
386
 
366
387
  ```bash
@@ -431,6 +452,8 @@ pybun self update --dry-run
431
452
  pybun self update --channel nightly
432
453
  ```
433
454
 
455
+ MCP exposes the same doctor remediation plan: call `pybun_doctor` with `{"fix": true}` to preview, or `{"fix": true, "apply": true}` to apply low-risk, auto-applicable fixes. `applied_fixes` includes before/after cache counts; `skipped_fixes` explains candidates requiring manual action. The existing `pybun_fix` tool continues to fix lint violations with Ruff.
456
+
434
457
  ## Sandbox usage
435
458
 
436
459
  Use the sandbox for untrusted scripts or PEP 723 snippets:
@@ -438,7 +461,34 @@ Use the sandbox for untrusted scripts or PEP 723 snippets:
438
461
  pybun --format=json run --sandbox examples/hello.py
439
462
  pybun --format=json run --sandbox --allow-network -c "print('net ok')"
440
463
  ```
441
- The sandbox isolates file and network access; add `--allow-network` only when required. Combine with `--profile=prod` for production-like runs.
464
+ Use repeatable `--allow-net=host[:port]` to permit selected network destinations
465
+ and `--allow-run=executable-name-or-path` to permit trusted executables. For example:
466
+
467
+ ```bash
468
+ pybun --format=json run --sandbox --allow-net=pypi.org:443 script.py
469
+ pybun --format=json run --sandbox --allow-run=/usr/bin/git script.py
470
+ ```
471
+
472
+ `--allow-network` retains unrestricted network access. Hostnames are resolved
473
+ once at startup; permission includes those IP addresses (and hosts sharing them).
474
+ Ports are exact when provided; omit the port to allow all ports for that host.
475
+ Use brackets for IPv6, for example `[::1]:8080`. TCP and UDP sends, direct
476
+ connects, and DNS lookups are checked. Denials identify the target with
477
+ `E_SANDBOX_NET_DENIED` or `E_SANDBOX_RUN_DENIED` and a permission hint.
478
+ MCP `sandbox_policy` supports matching `allow_net` and `allow_run` arrays.
479
+
480
+ Executable names are resolved from the initial PATH and pinned to their real
481
+ paths. Subprocess PATH/cwd overrides cannot authorize a different executable.
482
+ Use explicit paths for direct exec/posix_spawn calls; raw fork and shell-command
483
+ APIs remain denied. An allowed executable is trusted: it can launch children and
484
+ access the host independently of Python guards. When `allow_run` is nonempty,
485
+ the blanket zero-child OS process limit is not applied; `resource_limits.unsupported`
486
+ reports `process`. Existing parent OS limits still apply.
487
+
488
+ These are Python runtime guards plus supported resource limits, not a kernel
489
+ containment boundary. Native extensions and trusted external programs require
490
+ an OS/container sandbox when hostile code containment is needed. Combine with
491
+ `--profile=prod` for production-like runs.
442
492
 
443
493
  ## Profiles
444
494
 
@@ -506,6 +556,121 @@ Requires `pip install pybun-cli`.
506
556
  ```
507
557
  *Note: If `pybun` is not in the PATH, provide the absolute path (e.g., `/Users/username/bin/pybun`).*
508
558
 
559
+ ### Server policy
560
+
561
+ MCP execution requires an operator-owned security policy. Configure it before
562
+ starting the server; requests cannot reload or override it. The server freezes
563
+ one configuration at startup, with this precedence: `PYBUN_MCP_POLICY` file,
564
+ then `.pybun/config.toml`, then `pyproject.toml`. A selected file with a missing
565
+ or malformed policy fails closed; it does not fall back to another file.
566
+ `pyproject.toml` and explicit files use `[tool.pybun.mcp.policy]`;
567
+ `.pybun/config.toml` also accepts `[mcp.policy]`.
568
+
569
+ For a restricted native execution server:
570
+
571
+ ```toml
572
+ [tool.pybun.mcp.policy]
573
+ default_sandbox = true
574
+ require_sandbox = true
575
+ allow_network = false
576
+ max_execution_time_s = 30
577
+ max_memory_mb = 0 # Linux supports a nonzero memory cap
578
+ max_cpu_time_s = 0
579
+ allowed_working_directories = ["."]
580
+ allowed_backends = ["native"]
581
+ denied_tools = ["pybun_gc"]
582
+ ```
583
+
584
+ Start it with `PYBUN_BACKEND=native pybun mcp serve --stdio`. Relative boundaries
585
+ are resolved against the startup directory; start at the workspace root or
586
+ explicitly authorize its ancestor workspace. Keep policy files under operator
587
+ control, outside agent-writable paths for deployments. Audit logs must also be
588
+ outside the effective writable project roots.
589
+
590
+ Every tool and its resource aliases pass through authorization before audit
591
+ setup, filesystem snapshots or execution. Responses expose `policy_applied` and
592
+ stable `E_MCP_POLICY_*` denial codes. Requested limits may be stricter; zero
593
+ (unlimited) cannot relax an operator cap. Required sandboxing and active caps
594
+ prevent `unsafe_no_sandbox` from bypassing enforcement. Disallowed network
595
+ permission also removes granular network and executable grants. Sandbox reads
596
+ and writes default to the startup project boundary; explicit paths, symlinks,
597
+ backend-derived workspaces and environment/lock outputs are checked against the
598
+ operator's authorized directories. System-environment installation is denied.
599
+ Write snapshots skip symlinks and generated trees (`target`, `.git`, `.venv`,
600
+ `venv`, `__pycache__`, `node_modules`), and are bounded to 2,000 entries, depth
601
+ 32, 4 MiB per file and 16 MiB read per snapshot. Audit records expose
602
+ `file_snapshot_complete=false` whenever evidence is skipped or bounded; their
603
+ `file_writes` then describes only observed changes, not a complete write trace.
604
+ Pure in-memory environment comparisons remain available without execution policy.
605
+
606
+ Enforcement support is explicit: native inline code and ordinary scripts support
607
+ these execution limits. Bounded uv preparation and native PEP 723 preparation
608
+ cannot currently propagate all server limits, so incompatible calls return
609
+ `E_MCP_POLICY_UNSUPPORTED` before preparation. Other tool handlers that cannot
610
+ enforce a requested sandbox/network/resource restriction also return that code.
611
+ Memory caps are unavailable on macOS/Windows; CPU caps require Unix. A dry run
612
+ can preview a bounded uv call without starting the backend.
613
+
614
+ Operators who authorize trusted uv project operations can set
615
+ `require_sandbox=false`, `allow_network=true`, `max_execution_time_s=0`,
616
+ `max_memory_mb=0`, and `max_cpu_time_s=0`; execution-time restrictions that a tool
617
+ cannot enforce must be disabled explicitly. Its existing backend deadlines still
618
+ apply. This does not grant requests permission to escape working-directory or
619
+ backend boundaries. The Python guard's documented containment limits still apply.
620
+
621
+ ### Previewing execution
622
+
623
+ Call `pybun_run` with `dry_run: true` to return `would_execute`,
624
+ `estimated_risk` (`low`, `medium`, or `high`), `risk_reasons`, and
625
+ `sandbox_policy_that_would_apply` without executing code or preparing a backend.
626
+ The existing `risk_level` and `sandbox_policy` fields remain equivalent aliases.
627
+ The response also includes server policy and admission limits; previews consume
628
+ normal tool quota. Previewing an unsupported bounded backend reports
629
+ `policy_applied.execution_supported=false` and its denial code.
630
+
631
+ Risk is a substring heuristic over inline code or at most 2 MiB of regular UTF-8
632
+ script source. Missing script targets are rejected. Non-text, oversized or unreadable source reports
633
+ uncertainty as `medium`; filenames are not treated as source. If both are supplied,
634
+ inline code takes precedence, matching execution. `low` is not proof of safety:
635
+ dynamic imports, called code and later file changes can alter behavior. A preview
636
+ does not grant execution permission; the actual call is authorized independently.
637
+
638
+ ### Tool rate limits
639
+
640
+ The same frozen configuration accepts `[tool.pybun.mcp.rate_limit]` (or
641
+ `[mcp.rate_limit]` in `.pybun/config.toml`):
642
+
643
+ ```toml
644
+ [tool.pybun.mcp.rate_limit]
645
+ calls_per_minute = 60 # all calls in one stdio session
646
+ global_calls_per_minute = 600 # all sessions in this process/configuration
647
+ pybun_run_per_minute = 10
648
+ pybun_install_per_hour = 20
649
+
650
+ [tool.pybun.mcp.rate_limit.per_tool.pybun_lock]
651
+ calls = 5
652
+ window_secs = 60
653
+ ```
654
+
655
+ These are token buckets: each starts with a full window's burst and refills
656
+ continuously using monotonic time. All applicable buckets are checked and debited
657
+ atomically after policy authorization, before filesystem snapshots, backend work
658
+ or subprocesses. Resource aliases share the corresponding tool's bucket. A denied
659
+ call debits no bucket; cancelling an admitted call retains its debit. Rejections
660
+ return `mcp.rate_limit_exceeded`, positive `retry_after_ms`, scope and applicable
661
+ limits in `rate_limit_applied`, and write a security audit event without invoking
662
+ the tool or producing its success receipt. Admitted responses expose their limits.
663
+
664
+ One stdio server owns one generated session UUID for its lifetime; client IDs and
665
+ arguments cannot change it. Global/per-tool quotas are shared by server instances
666
+ in the same process with the same canonical startup directory and frozen rate
667
+ configuration. Separate processes and restarts do not share quota state. State
668
+ is bounded to 64 configurations and 512 sessions per configuration; live entries
669
+ are never evicted to reset a quota. Idle sessions expire after two minutes and
670
+ configurations after twice their longest tool window (at least two hours).
671
+ Capacity exhaustion returns a retry hint. Configuration rejects unknown tool names
672
+ and fields, zero or more than 1,000,000 calls, and windows outside 1–86,400 seconds.
673
+
509
674
  ## JSON output examples
510
675
 
511
676
  All commands support the `--format=json` option (schema v1). Examples:
@@ -679,3 +844,65 @@ Environment override: `PYBUN_TELEMETRY=0|1`
679
844
  ## License
680
845
 
681
846
  MIT
847
+
848
+ ### Local execution history
849
+
850
+ `pybun history --since 20m --format=json` queries CLI receipts for `run`, `test`,
851
+ `install`, `add`, and `remove`, including failed invocations. Receipts are appended
852
+ under `PYBUN_HOME/history.jsonl` (default `~/.cache/pybun/history.jsonl`), with a
853
+ private file mode on Unix and a file lock for concurrent writers. Existing files
854
+ with group/other permissions are rejected for appends; restrict them to mode 600. Set
855
+ `PYBUN_HISTORY=0` to disable recording. History queries never record themselves;
856
+ recording failures warn without changing the command's exit code.
857
+
858
+ Records contain timestamps, duration, exit code, lock digest when available,
859
+ backend identity, Python version/ABI, and known cache/policy/profile evidence.
860
+ Python observations identify whether they describe the execution interpreter or
861
+ the selected environment observed after the command; unavailable evidence is
862
+ `null`. Daemon use is false for the current direct execution paths. Inline code
863
+ and credential option values are removed, and the existing output redaction rules
864
+ apply before persistence and on reads. Program output and environment variables
865
+ are never stored. Arbitrary positional secrets cannot be inferred; disable
866
+ history when arguments contain such values. Queries accept positive `s`, `m`,
867
+ `h`, or `d` durations, report corrupt/partial lines without echoing them, and
868
+ reject files over 32 MiB; archive the local log when needed.
869
+
870
+ ### Comparing captured environments
871
+
872
+ `pybun why <package> --format=json` explains every known declaration path to a
873
+ locked distribution. MCP `pybun_why` takes a `package` string and uses the same
874
+ service. The uv backend reads frozen, offline `uv workspace metadata`; it does
875
+ not resolve, install, or synchronize an environment. Paths retain markers and
876
+ classify direct, transitive, extra, dependency-group, and workspace provenance.
877
+ They describe lock alternatives rather than the currently active environment.
878
+ Results include backend/version, lock digest, `complete`, and diagnostics. An
879
+ unreachable or unavailable locked variant is explicitly `unknown`. Native
880
+ provenance uses current declarations with its lock and always reports partial
881
+ evidence. Traversal is bounded (10,000 nodes, 100,000 expansions, depth 256,
882
+ 1,000 paths); reaching a bound also reports partial evidence.
883
+
884
+ `pybun environment diff before.json after.json --format=json` compares two JSON
885
+ captures, raw execution receipts, or JSON command envelopes. The MCP equivalent is
886
+ `pybun_environment_diff` with `before` and `after` objects; both use the same
887
+ read-only service. To compare CLI history entries, save the chosen `records[]`
888
+ objects from `pybun history --format=json` as the input files. Each input file is
889
+ limited to 4 MiB.
890
+
891
+ The response reports deterministic `changes` with reason codes for Python,
892
+ backend, lock, package, policy, profile and project evidence. `installed_packages`
893
+ describes interpreter-site inventory; `packages` separately describes a backend's
894
+ resolved lock inventory, which does not prove installation. History captures
895
+ standard isolated-venv site packages by reading distribution metadata without
896
+ importing project packages or running site hooks. System Python and venvs that
897
+ include system/user sites leave the installed inventory unavailable. Executable
898
+ digests are captured when readable; wheel artifact hashes remain unavailable
899
+ unless supplied by a capture.
900
+
901
+ Missing, masked and explicitly unknown values appear in `unavailable` and warning
902
+ diagnostics. They do not establish equality or a runtime state change. An identical
903
+ partial capture has an empty `changes` array while still reporting missing evidence.
904
+ Array ordering and normalized package spelling do not create differences. Only
905
+ typed, allowlisted metadata is compared; unrestricted environment variables and
906
+ unknown configuration keys are excluded, and existing redaction applies before
907
+ comparison. `causality: "not_inferred"` means the result describes recorded state
908
+ changes and never claims that they caused a program's output to change.
@@ -1,5 +1,12 @@
1
1
  # PyBun (Python Bundle)
2
2
 
3
+ # v0.2 backend transition
4
+
5
+ Normal project install/add/remove/lock/upgrade and project/PEP 723 execution now
6
+ delegate to uv. See [backend semantics and migration](docs/UV_BACKEND.md) for
7
+ the supported uv version, lock ownership, JSON v2, MCP parity, and explicit
8
+ native compatibility mode.
9
+
3
10
  <p align="center">
4
11
  <strong>🐍 The Agent-Native Python Control Plane 🤖</strong>
5
12
  </p>
@@ -244,6 +251,18 @@ pybun x cowsay==6.1
244
251
  pybun x black -- --check .
245
252
  ```
246
253
 
254
+ ### Affected Tests
255
+
256
+ ```bash
257
+ pybun test --affected
258
+ pybun test --affected --since main --format=json
259
+ pybun test --affected --discover --format=json
260
+ ```
261
+
262
+ `--changed` is an alias for `--affected`. Selection is repository-wide and cannot be combined with explicit paths or `--member`. MCP `pybun_test` uses the same service with `{"changed": true, "since": "main"}`.
263
+
264
+ JSON includes `selection` with `direct`, `transitive`, or `conservative` reasons and import chains. The target Python's standard AST parser reads project source without executing it. Imports and ancestor `conftest.py` dependencies are modeled; uncertain parsing, known dynamic imports, or non-Python/configuration changes conservatively select all tests. Import-root ambiguity may select extra tests. Arbitrary runtime file dependencies are not inferred. Unavailable git/runtime evidence returns an error rather than a verified empty selection.
265
+
247
266
  ### Python Version Management
248
267
 
249
268
  ```bash
@@ -341,6 +360,8 @@ pybun audit --severity-threshold=medium
341
360
  pybun audit --fail-on=high
342
361
  ```
343
362
 
363
+ The scanner follows OSV batch pagination and fetches advisory details before filtering or suggesting fixes. Missing severity is reported as `unknown`, remains visible at every threshold, and fails any `--fail-on` gate. Failed advisory lookups fail the scan. `PYBUN_OSV_URL` can point to a mirror's batch endpoint; its sibling `vulns/<id>` endpoint must serve full advisories.
364
+
344
365
  ### Profile Management
345
366
 
346
367
  ```bash
@@ -411,6 +432,8 @@ pybun self update --dry-run
411
432
  pybun self update --channel nightly
412
433
  ```
413
434
 
435
+ MCP exposes the same doctor remediation plan: call `pybun_doctor` with `{"fix": true}` to preview, or `{"fix": true, "apply": true}` to apply low-risk, auto-applicable fixes. `applied_fixes` includes before/after cache counts; `skipped_fixes` explains candidates requiring manual action. The existing `pybun_fix` tool continues to fix lint violations with Ruff.
436
+
414
437
  ## Sandbox usage
415
438
 
416
439
  Use the sandbox for untrusted scripts or PEP 723 snippets:
@@ -418,7 +441,34 @@ Use the sandbox for untrusted scripts or PEP 723 snippets:
418
441
  pybun --format=json run --sandbox examples/hello.py
419
442
  pybun --format=json run --sandbox --allow-network -c "print('net ok')"
420
443
  ```
421
- The sandbox isolates file and network access; add `--allow-network` only when required. Combine with `--profile=prod` for production-like runs.
444
+ Use repeatable `--allow-net=host[:port]` to permit selected network destinations
445
+ and `--allow-run=executable-name-or-path` to permit trusted executables. For example:
446
+
447
+ ```bash
448
+ pybun --format=json run --sandbox --allow-net=pypi.org:443 script.py
449
+ pybun --format=json run --sandbox --allow-run=/usr/bin/git script.py
450
+ ```
451
+
452
+ `--allow-network` retains unrestricted network access. Hostnames are resolved
453
+ once at startup; permission includes those IP addresses (and hosts sharing them).
454
+ Ports are exact when provided; omit the port to allow all ports for that host.
455
+ Use brackets for IPv6, for example `[::1]:8080`. TCP and UDP sends, direct
456
+ connects, and DNS lookups are checked. Denials identify the target with
457
+ `E_SANDBOX_NET_DENIED` or `E_SANDBOX_RUN_DENIED` and a permission hint.
458
+ MCP `sandbox_policy` supports matching `allow_net` and `allow_run` arrays.
459
+
460
+ Executable names are resolved from the initial PATH and pinned to their real
461
+ paths. Subprocess PATH/cwd overrides cannot authorize a different executable.
462
+ Use explicit paths for direct exec/posix_spawn calls; raw fork and shell-command
463
+ APIs remain denied. An allowed executable is trusted: it can launch children and
464
+ access the host independently of Python guards. When `allow_run` is nonempty,
465
+ the blanket zero-child OS process limit is not applied; `resource_limits.unsupported`
466
+ reports `process`. Existing parent OS limits still apply.
467
+
468
+ These are Python runtime guards plus supported resource limits, not a kernel
469
+ containment boundary. Native extensions and trusted external programs require
470
+ an OS/container sandbox when hostile code containment is needed. Combine with
471
+ `--profile=prod` for production-like runs.
422
472
 
423
473
  ## Profiles
424
474
 
@@ -486,6 +536,121 @@ Requires `pip install pybun-cli`.
486
536
  ```
487
537
  *Note: If `pybun` is not in the PATH, provide the absolute path (e.g., `/Users/username/bin/pybun`).*
488
538
 
539
+ ### Server policy
540
+
541
+ MCP execution requires an operator-owned security policy. Configure it before
542
+ starting the server; requests cannot reload or override it. The server freezes
543
+ one configuration at startup, with this precedence: `PYBUN_MCP_POLICY` file,
544
+ then `.pybun/config.toml`, then `pyproject.toml`. A selected file with a missing
545
+ or malformed policy fails closed; it does not fall back to another file.
546
+ `pyproject.toml` and explicit files use `[tool.pybun.mcp.policy]`;
547
+ `.pybun/config.toml` also accepts `[mcp.policy]`.
548
+
549
+ For a restricted native execution server:
550
+
551
+ ```toml
552
+ [tool.pybun.mcp.policy]
553
+ default_sandbox = true
554
+ require_sandbox = true
555
+ allow_network = false
556
+ max_execution_time_s = 30
557
+ max_memory_mb = 0 # Linux supports a nonzero memory cap
558
+ max_cpu_time_s = 0
559
+ allowed_working_directories = ["."]
560
+ allowed_backends = ["native"]
561
+ denied_tools = ["pybun_gc"]
562
+ ```
563
+
564
+ Start it with `PYBUN_BACKEND=native pybun mcp serve --stdio`. Relative boundaries
565
+ are resolved against the startup directory; start at the workspace root or
566
+ explicitly authorize its ancestor workspace. Keep policy files under operator
567
+ control, outside agent-writable paths for deployments. Audit logs must also be
568
+ outside the effective writable project roots.
569
+
570
+ Every tool and its resource aliases pass through authorization before audit
571
+ setup, filesystem snapshots or execution. Responses expose `policy_applied` and
572
+ stable `E_MCP_POLICY_*` denial codes. Requested limits may be stricter; zero
573
+ (unlimited) cannot relax an operator cap. Required sandboxing and active caps
574
+ prevent `unsafe_no_sandbox` from bypassing enforcement. Disallowed network
575
+ permission also removes granular network and executable grants. Sandbox reads
576
+ and writes default to the startup project boundary; explicit paths, symlinks,
577
+ backend-derived workspaces and environment/lock outputs are checked against the
578
+ operator's authorized directories. System-environment installation is denied.
579
+ Write snapshots skip symlinks and generated trees (`target`, `.git`, `.venv`,
580
+ `venv`, `__pycache__`, `node_modules`), and are bounded to 2,000 entries, depth
581
+ 32, 4 MiB per file and 16 MiB read per snapshot. Audit records expose
582
+ `file_snapshot_complete=false` whenever evidence is skipped or bounded; their
583
+ `file_writes` then describes only observed changes, not a complete write trace.
584
+ Pure in-memory environment comparisons remain available without execution policy.
585
+
586
+ Enforcement support is explicit: native inline code and ordinary scripts support
587
+ these execution limits. Bounded uv preparation and native PEP 723 preparation
588
+ cannot currently propagate all server limits, so incompatible calls return
589
+ `E_MCP_POLICY_UNSUPPORTED` before preparation. Other tool handlers that cannot
590
+ enforce a requested sandbox/network/resource restriction also return that code.
591
+ Memory caps are unavailable on macOS/Windows; CPU caps require Unix. A dry run
592
+ can preview a bounded uv call without starting the backend.
593
+
594
+ Operators who authorize trusted uv project operations can set
595
+ `require_sandbox=false`, `allow_network=true`, `max_execution_time_s=0`,
596
+ `max_memory_mb=0`, and `max_cpu_time_s=0`; execution-time restrictions that a tool
597
+ cannot enforce must be disabled explicitly. Its existing backend deadlines still
598
+ apply. This does not grant requests permission to escape working-directory or
599
+ backend boundaries. The Python guard's documented containment limits still apply.
600
+
601
+ ### Previewing execution
602
+
603
+ Call `pybun_run` with `dry_run: true` to return `would_execute`,
604
+ `estimated_risk` (`low`, `medium`, or `high`), `risk_reasons`, and
605
+ `sandbox_policy_that_would_apply` without executing code or preparing a backend.
606
+ The existing `risk_level` and `sandbox_policy` fields remain equivalent aliases.
607
+ The response also includes server policy and admission limits; previews consume
608
+ normal tool quota. Previewing an unsupported bounded backend reports
609
+ `policy_applied.execution_supported=false` and its denial code.
610
+
611
+ Risk is a substring heuristic over inline code or at most 2 MiB of regular UTF-8
612
+ script source. Missing script targets are rejected. Non-text, oversized or unreadable source reports
613
+ uncertainty as `medium`; filenames are not treated as source. If both are supplied,
614
+ inline code takes precedence, matching execution. `low` is not proof of safety:
615
+ dynamic imports, called code and later file changes can alter behavior. A preview
616
+ does not grant execution permission; the actual call is authorized independently.
617
+
618
+ ### Tool rate limits
619
+
620
+ The same frozen configuration accepts `[tool.pybun.mcp.rate_limit]` (or
621
+ `[mcp.rate_limit]` in `.pybun/config.toml`):
622
+
623
+ ```toml
624
+ [tool.pybun.mcp.rate_limit]
625
+ calls_per_minute = 60 # all calls in one stdio session
626
+ global_calls_per_minute = 600 # all sessions in this process/configuration
627
+ pybun_run_per_minute = 10
628
+ pybun_install_per_hour = 20
629
+
630
+ [tool.pybun.mcp.rate_limit.per_tool.pybun_lock]
631
+ calls = 5
632
+ window_secs = 60
633
+ ```
634
+
635
+ These are token buckets: each starts with a full window's burst and refills
636
+ continuously using monotonic time. All applicable buckets are checked and debited
637
+ atomically after policy authorization, before filesystem snapshots, backend work
638
+ or subprocesses. Resource aliases share the corresponding tool's bucket. A denied
639
+ call debits no bucket; cancelling an admitted call retains its debit. Rejections
640
+ return `mcp.rate_limit_exceeded`, positive `retry_after_ms`, scope and applicable
641
+ limits in `rate_limit_applied`, and write a security audit event without invoking
642
+ the tool or producing its success receipt. Admitted responses expose their limits.
643
+
644
+ One stdio server owns one generated session UUID for its lifetime; client IDs and
645
+ arguments cannot change it. Global/per-tool quotas are shared by server instances
646
+ in the same process with the same canonical startup directory and frozen rate
647
+ configuration. Separate processes and restarts do not share quota state. State
648
+ is bounded to 64 configurations and 512 sessions per configuration; live entries
649
+ are never evicted to reset a quota. Idle sessions expire after two minutes and
650
+ configurations after twice their longest tool window (at least two hours).
651
+ Capacity exhaustion returns a retry hint. Configuration rejects unknown tool names
652
+ and fields, zero or more than 1,000,000 calls, and windows outside 1–86,400 seconds.
653
+
489
654
  ## JSON output examples
490
655
 
491
656
  All commands support the `--format=json` option (schema v1). Examples:
@@ -659,3 +824,65 @@ Environment override: `PYBUN_TELEMETRY=0|1`
659
824
  ## License
660
825
 
661
826
  MIT
827
+
828
+ ### Local execution history
829
+
830
+ `pybun history --since 20m --format=json` queries CLI receipts for `run`, `test`,
831
+ `install`, `add`, and `remove`, including failed invocations. Receipts are appended
832
+ under `PYBUN_HOME/history.jsonl` (default `~/.cache/pybun/history.jsonl`), with a
833
+ private file mode on Unix and a file lock for concurrent writers. Existing files
834
+ with group/other permissions are rejected for appends; restrict them to mode 600. Set
835
+ `PYBUN_HISTORY=0` to disable recording. History queries never record themselves;
836
+ recording failures warn without changing the command's exit code.
837
+
838
+ Records contain timestamps, duration, exit code, lock digest when available,
839
+ backend identity, Python version/ABI, and known cache/policy/profile evidence.
840
+ Python observations identify whether they describe the execution interpreter or
841
+ the selected environment observed after the command; unavailable evidence is
842
+ `null`. Daemon use is false for the current direct execution paths. Inline code
843
+ and credential option values are removed, and the existing output redaction rules
844
+ apply before persistence and on reads. Program output and environment variables
845
+ are never stored. Arbitrary positional secrets cannot be inferred; disable
846
+ history when arguments contain such values. Queries accept positive `s`, `m`,
847
+ `h`, or `d` durations, report corrupt/partial lines without echoing them, and
848
+ reject files over 32 MiB; archive the local log when needed.
849
+
850
+ ### Comparing captured environments
851
+
852
+ `pybun why <package> --format=json` explains every known declaration path to a
853
+ locked distribution. MCP `pybun_why` takes a `package` string and uses the same
854
+ service. The uv backend reads frozen, offline `uv workspace metadata`; it does
855
+ not resolve, install, or synchronize an environment. Paths retain markers and
856
+ classify direct, transitive, extra, dependency-group, and workspace provenance.
857
+ They describe lock alternatives rather than the currently active environment.
858
+ Results include backend/version, lock digest, `complete`, and diagnostics. An
859
+ unreachable or unavailable locked variant is explicitly `unknown`. Native
860
+ provenance uses current declarations with its lock and always reports partial
861
+ evidence. Traversal is bounded (10,000 nodes, 100,000 expansions, depth 256,
862
+ 1,000 paths); reaching a bound also reports partial evidence.
863
+
864
+ `pybun environment diff before.json after.json --format=json` compares two JSON
865
+ captures, raw execution receipts, or JSON command envelopes. The MCP equivalent is
866
+ `pybun_environment_diff` with `before` and `after` objects; both use the same
867
+ read-only service. To compare CLI history entries, save the chosen `records[]`
868
+ objects from `pybun history --format=json` as the input files. Each input file is
869
+ limited to 4 MiB.
870
+
871
+ The response reports deterministic `changes` with reason codes for Python,
872
+ backend, lock, package, policy, profile and project evidence. `installed_packages`
873
+ describes interpreter-site inventory; `packages` separately describes a backend's
874
+ resolved lock inventory, which does not prove installation. History captures
875
+ standard isolated-venv site packages by reading distribution metadata without
876
+ importing project packages or running site hooks. System Python and venvs that
877
+ include system/user sites leave the installed inventory unavailable. Executable
878
+ digests are captured when readable; wheel artifact hashes remain unavailable
879
+ unless supplied by a capture.
880
+
881
+ Missing, masked and explicitly unknown values appear in `unavailable` and warning
882
+ diagnostics. They do not establish equality or a runtime state change. An identical
883
+ partial capture has an empty `changes` array while still reporting missing evidence.
884
+ Array ordering and normalized package spelling do not create differences. Only
885
+ typed, allowlisted metadata is compared; unrestricted environment variables and
886
+ unknown configuration keys are excluded, and existing redaction applies before
887
+ comparison. `causality: "not_inferred"` means the result describes recorded state
888
+ changes and never claims that they caused a program's output to change.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pybun-cli
3
- Version: 0.1.28
3
+ Version: 0.2.1
4
4
  Summary: PyBun shim bootstrapper for signed releases
5
5
  Author: PyBun Authors
6
6
  License-Expression: MIT
@@ -20,6 +20,13 @@ Dynamic: license-file
20
20
 
21
21
  # PyBun (Python Bundle)
22
22
 
23
+ # v0.2 backend transition
24
+
25
+ Normal project install/add/remove/lock/upgrade and project/PEP 723 execution now
26
+ delegate to uv. See [backend semantics and migration](docs/UV_BACKEND.md) for
27
+ the supported uv version, lock ownership, JSON v2, MCP parity, and explicit
28
+ native compatibility mode.
29
+
23
30
  <p align="center">
24
31
  <strong>🐍 The Agent-Native Python Control Plane 🤖</strong>
25
32
  </p>
@@ -264,6 +271,18 @@ pybun x cowsay==6.1
264
271
  pybun x black -- --check .
265
272
  ```
266
273
 
274
+ ### Affected Tests
275
+
276
+ ```bash
277
+ pybun test --affected
278
+ pybun test --affected --since main --format=json
279
+ pybun test --affected --discover --format=json
280
+ ```
281
+
282
+ `--changed` is an alias for `--affected`. Selection is repository-wide and cannot be combined with explicit paths or `--member`. MCP `pybun_test` uses the same service with `{"changed": true, "since": "main"}`.
283
+
284
+ JSON includes `selection` with `direct`, `transitive`, or `conservative` reasons and import chains. The target Python's standard AST parser reads project source without executing it. Imports and ancestor `conftest.py` dependencies are modeled; uncertain parsing, known dynamic imports, or non-Python/configuration changes conservatively select all tests. Import-root ambiguity may select extra tests. Arbitrary runtime file dependencies are not inferred. Unavailable git/runtime evidence returns an error rather than a verified empty selection.
285
+
267
286
  ### Python Version Management
268
287
 
269
288
  ```bash
@@ -361,6 +380,8 @@ pybun audit --severity-threshold=medium
361
380
  pybun audit --fail-on=high
362
381
  ```
363
382
 
383
+ The scanner follows OSV batch pagination and fetches advisory details before filtering or suggesting fixes. Missing severity is reported as `unknown`, remains visible at every threshold, and fails any `--fail-on` gate. Failed advisory lookups fail the scan. `PYBUN_OSV_URL` can point to a mirror's batch endpoint; its sibling `vulns/<id>` endpoint must serve full advisories.
384
+
364
385
  ### Profile Management
365
386
 
366
387
  ```bash
@@ -431,6 +452,8 @@ pybun self update --dry-run
431
452
  pybun self update --channel nightly
432
453
  ```
433
454
 
455
+ MCP exposes the same doctor remediation plan: call `pybun_doctor` with `{"fix": true}` to preview, or `{"fix": true, "apply": true}` to apply low-risk, auto-applicable fixes. `applied_fixes` includes before/after cache counts; `skipped_fixes` explains candidates requiring manual action. The existing `pybun_fix` tool continues to fix lint violations with Ruff.
456
+
434
457
  ## Sandbox usage
435
458
 
436
459
  Use the sandbox for untrusted scripts or PEP 723 snippets:
@@ -438,7 +461,34 @@ Use the sandbox for untrusted scripts or PEP 723 snippets:
438
461
  pybun --format=json run --sandbox examples/hello.py
439
462
  pybun --format=json run --sandbox --allow-network -c "print('net ok')"
440
463
  ```
441
- The sandbox isolates file and network access; add `--allow-network` only when required. Combine with `--profile=prod` for production-like runs.
464
+ Use repeatable `--allow-net=host[:port]` to permit selected network destinations
465
+ and `--allow-run=executable-name-or-path` to permit trusted executables. For example:
466
+
467
+ ```bash
468
+ pybun --format=json run --sandbox --allow-net=pypi.org:443 script.py
469
+ pybun --format=json run --sandbox --allow-run=/usr/bin/git script.py
470
+ ```
471
+
472
+ `--allow-network` retains unrestricted network access. Hostnames are resolved
473
+ once at startup; permission includes those IP addresses (and hosts sharing them).
474
+ Ports are exact when provided; omit the port to allow all ports for that host.
475
+ Use brackets for IPv6, for example `[::1]:8080`. TCP and UDP sends, direct
476
+ connects, and DNS lookups are checked. Denials identify the target with
477
+ `E_SANDBOX_NET_DENIED` or `E_SANDBOX_RUN_DENIED` and a permission hint.
478
+ MCP `sandbox_policy` supports matching `allow_net` and `allow_run` arrays.
479
+
480
+ Executable names are resolved from the initial PATH and pinned to their real
481
+ paths. Subprocess PATH/cwd overrides cannot authorize a different executable.
482
+ Use explicit paths for direct exec/posix_spawn calls; raw fork and shell-command
483
+ APIs remain denied. An allowed executable is trusted: it can launch children and
484
+ access the host independently of Python guards. When `allow_run` is nonempty,
485
+ the blanket zero-child OS process limit is not applied; `resource_limits.unsupported`
486
+ reports `process`. Existing parent OS limits still apply.
487
+
488
+ These are Python runtime guards plus supported resource limits, not a kernel
489
+ containment boundary. Native extensions and trusted external programs require
490
+ an OS/container sandbox when hostile code containment is needed. Combine with
491
+ `--profile=prod` for production-like runs.
442
492
 
443
493
  ## Profiles
444
494
 
@@ -506,6 +556,121 @@ Requires `pip install pybun-cli`.
506
556
  ```
507
557
  *Note: If `pybun` is not in the PATH, provide the absolute path (e.g., `/Users/username/bin/pybun`).*
508
558
 
559
+ ### Server policy
560
+
561
+ MCP execution requires an operator-owned security policy. Configure it before
562
+ starting the server; requests cannot reload or override it. The server freezes
563
+ one configuration at startup, with this precedence: `PYBUN_MCP_POLICY` file,
564
+ then `.pybun/config.toml`, then `pyproject.toml`. A selected file with a missing
565
+ or malformed policy fails closed; it does not fall back to another file.
566
+ `pyproject.toml` and explicit files use `[tool.pybun.mcp.policy]`;
567
+ `.pybun/config.toml` also accepts `[mcp.policy]`.
568
+
569
+ For a restricted native execution server:
570
+
571
+ ```toml
572
+ [tool.pybun.mcp.policy]
573
+ default_sandbox = true
574
+ require_sandbox = true
575
+ allow_network = false
576
+ max_execution_time_s = 30
577
+ max_memory_mb = 0 # Linux supports a nonzero memory cap
578
+ max_cpu_time_s = 0
579
+ allowed_working_directories = ["."]
580
+ allowed_backends = ["native"]
581
+ denied_tools = ["pybun_gc"]
582
+ ```
583
+
584
+ Start it with `PYBUN_BACKEND=native pybun mcp serve --stdio`. Relative boundaries
585
+ are resolved against the startup directory; start at the workspace root or
586
+ explicitly authorize its ancestor workspace. Keep policy files under operator
587
+ control, outside agent-writable paths for deployments. Audit logs must also be
588
+ outside the effective writable project roots.
589
+
590
+ Every tool and its resource aliases pass through authorization before audit
591
+ setup, filesystem snapshots or execution. Responses expose `policy_applied` and
592
+ stable `E_MCP_POLICY_*` denial codes. Requested limits may be stricter; zero
593
+ (unlimited) cannot relax an operator cap. Required sandboxing and active caps
594
+ prevent `unsafe_no_sandbox` from bypassing enforcement. Disallowed network
595
+ permission also removes granular network and executable grants. Sandbox reads
596
+ and writes default to the startup project boundary; explicit paths, symlinks,
597
+ backend-derived workspaces and environment/lock outputs are checked against the
598
+ operator's authorized directories. System-environment installation is denied.
599
+ Write snapshots skip symlinks and generated trees (`target`, `.git`, `.venv`,
600
+ `venv`, `__pycache__`, `node_modules`), and are bounded to 2,000 entries, depth
601
+ 32, 4 MiB per file and 16 MiB read per snapshot. Audit records expose
602
+ `file_snapshot_complete=false` whenever evidence is skipped or bounded; their
603
+ `file_writes` then describes only observed changes, not a complete write trace.
604
+ Pure in-memory environment comparisons remain available without execution policy.
605
+
606
+ Enforcement support is explicit: native inline code and ordinary scripts support
607
+ these execution limits. Bounded uv preparation and native PEP 723 preparation
608
+ cannot currently propagate all server limits, so incompatible calls return
609
+ `E_MCP_POLICY_UNSUPPORTED` before preparation. Other tool handlers that cannot
610
+ enforce a requested sandbox/network/resource restriction also return that code.
611
+ Memory caps are unavailable on macOS/Windows; CPU caps require Unix. A dry run
612
+ can preview a bounded uv call without starting the backend.
613
+
614
+ Operators who authorize trusted uv project operations can set
615
+ `require_sandbox=false`, `allow_network=true`, `max_execution_time_s=0`,
616
+ `max_memory_mb=0`, and `max_cpu_time_s=0`; execution-time restrictions that a tool
617
+ cannot enforce must be disabled explicitly. Its existing backend deadlines still
618
+ apply. This does not grant requests permission to escape working-directory or
619
+ backend boundaries. The Python guard's documented containment limits still apply.
620
+
621
+ ### Previewing execution
622
+
623
+ Call `pybun_run` with `dry_run: true` to return `would_execute`,
624
+ `estimated_risk` (`low`, `medium`, or `high`), `risk_reasons`, and
625
+ `sandbox_policy_that_would_apply` without executing code or preparing a backend.
626
+ The existing `risk_level` and `sandbox_policy` fields remain equivalent aliases.
627
+ The response also includes server policy and admission limits; previews consume
628
+ normal tool quota. Previewing an unsupported bounded backend reports
629
+ `policy_applied.execution_supported=false` and its denial code.
630
+
631
+ Risk is a substring heuristic over inline code or at most 2 MiB of regular UTF-8
632
+ script source. Missing script targets are rejected. Non-text, oversized or unreadable source reports
633
+ uncertainty as `medium`; filenames are not treated as source. If both are supplied,
634
+ inline code takes precedence, matching execution. `low` is not proof of safety:
635
+ dynamic imports, called code and later file changes can alter behavior. A preview
636
+ does not grant execution permission; the actual call is authorized independently.
637
+
638
+ ### Tool rate limits
639
+
640
+ The same frozen configuration accepts `[tool.pybun.mcp.rate_limit]` (or
641
+ `[mcp.rate_limit]` in `.pybun/config.toml`):
642
+
643
+ ```toml
644
+ [tool.pybun.mcp.rate_limit]
645
+ calls_per_minute = 60 # all calls in one stdio session
646
+ global_calls_per_minute = 600 # all sessions in this process/configuration
647
+ pybun_run_per_minute = 10
648
+ pybun_install_per_hour = 20
649
+
650
+ [tool.pybun.mcp.rate_limit.per_tool.pybun_lock]
651
+ calls = 5
652
+ window_secs = 60
653
+ ```
654
+
655
+ These are token buckets: each starts with a full window's burst and refills
656
+ continuously using monotonic time. All applicable buckets are checked and debited
657
+ atomically after policy authorization, before filesystem snapshots, backend work
658
+ or subprocesses. Resource aliases share the corresponding tool's bucket. A denied
659
+ call debits no bucket; cancelling an admitted call retains its debit. Rejections
660
+ return `mcp.rate_limit_exceeded`, positive `retry_after_ms`, scope and applicable
661
+ limits in `rate_limit_applied`, and write a security audit event without invoking
662
+ the tool or producing its success receipt. Admitted responses expose their limits.
663
+
664
+ One stdio server owns one generated session UUID for its lifetime; client IDs and
665
+ arguments cannot change it. Global/per-tool quotas are shared by server instances
666
+ in the same process with the same canonical startup directory and frozen rate
667
+ configuration. Separate processes and restarts do not share quota state. State
668
+ is bounded to 64 configurations and 512 sessions per configuration; live entries
669
+ are never evicted to reset a quota. Idle sessions expire after two minutes and
670
+ configurations after twice their longest tool window (at least two hours).
671
+ Capacity exhaustion returns a retry hint. Configuration rejects unknown tool names
672
+ and fields, zero or more than 1,000,000 calls, and windows outside 1–86,400 seconds.
673
+
509
674
  ## JSON output examples
510
675
 
511
676
  All commands support the `--format=json` option (schema v1). Examples:
@@ -679,3 +844,65 @@ Environment override: `PYBUN_TELEMETRY=0|1`
679
844
  ## License
680
845
 
681
846
  MIT
847
+
848
+ ### Local execution history
849
+
850
+ `pybun history --since 20m --format=json` queries CLI receipts for `run`, `test`,
851
+ `install`, `add`, and `remove`, including failed invocations. Receipts are appended
852
+ under `PYBUN_HOME/history.jsonl` (default `~/.cache/pybun/history.jsonl`), with a
853
+ private file mode on Unix and a file lock for concurrent writers. Existing files
854
+ with group/other permissions are rejected for appends; restrict them to mode 600. Set
855
+ `PYBUN_HISTORY=0` to disable recording. History queries never record themselves;
856
+ recording failures warn without changing the command's exit code.
857
+
858
+ Records contain timestamps, duration, exit code, lock digest when available,
859
+ backend identity, Python version/ABI, and known cache/policy/profile evidence.
860
+ Python observations identify whether they describe the execution interpreter or
861
+ the selected environment observed after the command; unavailable evidence is
862
+ `null`. Daemon use is false for the current direct execution paths. Inline code
863
+ and credential option values are removed, and the existing output redaction rules
864
+ apply before persistence and on reads. Program output and environment variables
865
+ are never stored. Arbitrary positional secrets cannot be inferred; disable
866
+ history when arguments contain such values. Queries accept positive `s`, `m`,
867
+ `h`, or `d` durations, report corrupt/partial lines without echoing them, and
868
+ reject files over 32 MiB; archive the local log when needed.
869
+
870
+ ### Comparing captured environments
871
+
872
+ `pybun why <package> --format=json` explains every known declaration path to a
873
+ locked distribution. MCP `pybun_why` takes a `package` string and uses the same
874
+ service. The uv backend reads frozen, offline `uv workspace metadata`; it does
875
+ not resolve, install, or synchronize an environment. Paths retain markers and
876
+ classify direct, transitive, extra, dependency-group, and workspace provenance.
877
+ They describe lock alternatives rather than the currently active environment.
878
+ Results include backend/version, lock digest, `complete`, and diagnostics. An
879
+ unreachable or unavailable locked variant is explicitly `unknown`. Native
880
+ provenance uses current declarations with its lock and always reports partial
881
+ evidence. Traversal is bounded (10,000 nodes, 100,000 expansions, depth 256,
882
+ 1,000 paths); reaching a bound also reports partial evidence.
883
+
884
+ `pybun environment diff before.json after.json --format=json` compares two JSON
885
+ captures, raw execution receipts, or JSON command envelopes. The MCP equivalent is
886
+ `pybun_environment_diff` with `before` and `after` objects; both use the same
887
+ read-only service. To compare CLI history entries, save the chosen `records[]`
888
+ objects from `pybun history --format=json` as the input files. Each input file is
889
+ limited to 4 MiB.
890
+
891
+ The response reports deterministic `changes` with reason codes for Python,
892
+ backend, lock, package, policy, profile and project evidence. `installed_packages`
893
+ describes interpreter-site inventory; `packages` separately describes a backend's
894
+ resolved lock inventory, which does not prove installation. History captures
895
+ standard isolated-venv site packages by reading distribution metadata without
896
+ importing project packages or running site hooks. System Python and venvs that
897
+ include system/user sites leave the installed inventory unavailable. Executable
898
+ digests are captured when readable; wheel artifact hashes remain unavailable
899
+ unless supplied by a capture.
900
+
901
+ Missing, masked and explicitly unknown values appear in `unavailable` and warning
902
+ diagnostics. They do not establish equality or a runtime state change. An identical
903
+ partial capture has an empty `changes` array while still reporting missing evidence.
904
+ Array ordering and normalized package spelling do not create differences. Only
905
+ typed, allowlisted metadata is compared; unrestricted environment variables and
906
+ unknown configuration keys are excluded, and existing redaction applies before
907
+ comparison. `causality: "not_inferred"` means the result describes recorded state
908
+ changes and never claims that they caused a program's output to change.
@@ -22,7 +22,7 @@ readme = { file = "README.md", content-type = "text/markdown" }
22
22
  requires-python = ">=3.8"
23
23
  license = "MIT"
24
24
  license-files = ["LICENSE"]
25
- version = "0.1.28"
25
+ version = "0.2.1"
26
26
 
27
27
  [[project.authors]]
28
28
  name = "PyBun Authors"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes