Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Agent-facing JSON path fields compact paths under the current home directory to
Core printer interaction is `BambuPrinter` in `bambu_cli/printer.py`. Agents and library users should instantiate it via the `get_printer()` factory (or `RuntimeContext.printer()`), not by manipulating globals.

- `BambuPrinter` handles FTPS and MQTT.
- Set `insecure_tls = False` and supply `cert_fingerprint` for MITM protection. Camera TLS (port 6000): the **direct grab** fails closed on a pin mismatch or on an `ssl.SSLError` during the handshake whenever a pin is configured, and without a pin it still refuses to send the access code over the direct connection — but `snapshot` then falls through to the Docker/RTSP streamer path, which has **no TLS verification** of its own; see [SECURITY.md](SECURITY.md) for the residual. MQTT/FTPS without a pin use system CA verification (`CERT_REQUIRED`), which fails for typical Bambu self-signed certs — still effectively fail-closed, but prefer an explicit pin. Pin match/mismatch is enforced when a fingerprint is configured.
- Set `insecure_tls = False` and supply `cert_fingerprint` for MITM protection. Camera TLS (port 6000): the **direct grab** fails closed on a pin mismatch or on an `ssl.SSLError` during the handshake whenever a pin is configured, and without a pin it still refuses to send the access code over the direct connection. The Docker/RTSP streamer (no TLS verification of its own) is **opt-in** via `camera_allow_streamer` or `--allow-camera-streamer`; the default is to abort. See [SECURITY.md](SECURITY.md). MQTT/FTPS without a pin use system CA verification (`CERT_REQUIRED`), which fails for typical Bambu self-signed certs — still effectively fail-closed, but prefer an explicit pin. Pin match/mismatch is enforced when a fingerprint is configured.
- `doctor` prints the live certificate fingerprint only when it is not yet pinned (or with `-v`); once pinned it prints a hex-free match confirmation, and the printer's LAN IP is redacted from human output unless `-v` is passed. `--json` always carries `certificate_fingerprint`. In an interactive TTY with no pin, doctor may offer to write `cert_fingerprint` into config.json. It never prompts in `--json` mode or non-interactive runs.
- Secret-bearing files are tightened to `0600` automatically on POSIX: config.json on load, and the `access_code_file` when `load_access_code()` reads it. Windows relies on NTFS ACLs (see [SECURITY.md](SECURITY.md)).
- Network operations support `timeout` and `retries` through `printer.send_command()` and `printer.status()`.
Expand Down Expand Up @@ -109,7 +109,7 @@ When adding tests, follow [docs/test-backlog.md](docs/test-backlog.md) and the q

## Camera snapshots for agents

`plate snapshot` captures a JPEG from the printer camera. To avoid stale-photo mistakes — where an agent re-sends a cached file instead of a fresh capture — always pass a fresh `--output` name or use `--unique` (generates `printer_snapshot_<UTC>Z.jpg`). Every successful `--json` response includes `sha256` (hex digest of the JPEG bytes) and `captured_at` (ISO-8601 UTC); compare these fields before sending the image to a user to verify the capture is genuinely new.
`plate snapshot` captures a JPEG from the printer camera. To avoid stale-photo mistakes — where an agent re-sends a cached file instead of a fresh capture — always pass a fresh `--output` name or use `--unique` (generates `printer_snapshot_<UTC>Z.jpg`). Every successful `--json` response includes `sha256` (hex digest of the JPEG bytes) and `captured_at` (ISO-8601 UTC); compare these fields before sending the image to a user to verify the capture is genuinely new. Do not pass `--allow-camera-streamer` unless the user asked: that path has no TLS pin.

## Agent usage

Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version

## [Unreleased]

### Changed

- **`plate snapshot` no longer falls back to the Docker streamer by default.**
The streamer does not honour `cert_fingerprint`, so a failed direct grab now
aborts unless you set `camera_allow_streamer` in config or pass
`--allow-camera-streamer`. X1-series printers need that opt-in.
`camera_direct_only` still forbids the streamer even when the opt-in is set.
The snapshot command moved out of `protocols/camera.py` into
`bambu_cli.commands.snapshot`.

## [0.5.0] - 2026-08-05

### Added
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ model URL or file → download → slice (OrcaSlicer) → upload → pri
one command: plate job <url> --confirm
```

**Supports:** any Bambu Lab printer with LAN mode — P1P, P1S, X1C, X1E, A1, A1 Mini. **Hardware-tested on the P1 series (P1P/P1S) only.** The rest speak the same LAN protocols and are expected to work, but are unverified on real hardware — treat them as best-effort and please [open an issue](https://github.com/DLANSAMA/platecli/issues) with what you hit. One caveat: `plate snapshot` grabs the camera directly (no extra software) on P1/A1-class printers, but X1-series cameras need a locally-running Docker streamer container.
**Supports:** any Bambu Lab printer with LAN mode — P1P, P1S, X1C, X1E, A1, A1 Mini. **Hardware-tested on the P1 series (P1P/P1S) only.** The rest speak the same LAN protocols and are expected to work, but are unverified on real hardware — treat them as best-effort and please [open an issue](https://github.com/DLANSAMA/platecli/issues) with what you hit. One caveat: `plate snapshot` grabs the camera directly (no extra software) on P1/A1-class printers. X1-series cameras need a locally-running Docker streamer, and that path is opt-in (`camera_allow_streamer` or `--allow-camera-streamer`) because the streamer does not honour `cert_fingerprint`.

## Install

Expand Down
10 changes: 5 additions & 5 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,10 @@ and (b) explicit model downloads you request. Key properties:
exists only as a last resort — it is never the default and the CLI warns when
it is used.
- **Camera (port 6000):** the *direct* grab refuses to proceed if neither pin nor
`insecure_tls` is set. Note that `snapshot` then falls back to the Docker streamer,
which does not honour the pin, so the command as a whole is not fail-closed by
default — set `camera_direct_only: true` to refuse that fallback (see
[Known limitations](#known-limitations)).
`insecure_tls` is set. The Docker streamer is **opt-in** (`camera_allow_streamer`
or `--allow-camera-streamer`) because it does not honour the pin. The default
is fail-closed: a failed direct grab aborts. `camera_direct_only: true` still
forbids the streamer even if the opt-in is also set.
- **MQTT / FTPS without a pin:** use system CA verification (`CERT_REQUIRED`),
which fails for typical Bambu self-signed certs (effective fail-closed). Prefer
an explicit pin for clear errors and uniform policy.
Expand Down Expand Up @@ -103,7 +103,7 @@ Tracked for hardening; not all are “bugs” in the sense of broken claims.
| Topic | Detail | Status |
|-------|--------|--------|
| **Camera Docker port bind** | Default `camera_port` is now `127.0.0.1:1985:1984`, so the streamer publishes the (unauthenticated) camera feed on **loopback only**. Set `camera_port` to `0.0.0.0:1985:1984` to deliberately expose it on the LAN. Host-qualified specs now parse correctly, `camera_port` is validated, and the CLI warns if a *pre-existing* container is still bound to a non-loopback interface (recreate with `docker rm -f bambu_camera`). | Fixed |
| **Camera pin fallback** | A pinned-fingerprint **mismatch**, and any `ssl.SSLError` from the direct grab — handshake or post-handshake — hard-abort the snapshot when a pin is configured. The `camera_direct_only` config key (default `false`) closes the remaining fallback routes: when set, any failure of the direct port-6000 grab — including no-pin SSLError, non-TLS connection failures (refused/reset/timeout), or a silent no-frame return — refuses to fall back to the Docker streamer and aborts with `EXIT_NETWORK_ERROR`. **What it does not cover:** (a) `camera_direct_only=true` with `insecure_tls=true` is direct-only but **completely unverified** — direct-only ≠ verified; verification requires a `cert_fingerprint`. (b) It stops platecli from using or starting the streamer, but an **already-running `bambu_camera` container keeps serving the unauthenticated feed** — run `docker rm -f bambu_camera` to stop it. (Re-running `plate setup` used to drop this hand-added key, silently disabling the control; setup now preserves every key it does not manage, and reports which ones it kept.) | Fixed |
| **Camera pin fallback** | Default is fail-closed: a failed direct grab does **not** start or use the Docker streamer. X1-series users must set `camera_allow_streamer: true` or pass `--allow-camera-streamer`. A pinned-fingerprint **mismatch**, and any `ssl.SSLError` from the direct grab when a pin is configured, still hard-abort (no streamer even if opted in). `camera_direct_only` remains a forbid switch that wins over the opt-in. **Residuals:** (a) `insecure_tls=true` is unverified whether or not the streamer is used. (b) An already-running `bambu_camera` container keeps serving the unauthenticated feed — run `docker rm -f bambu_camera` to stop it. Setup still preserves unmanaged keys. | Fixed |
| **HTTP downloads** | `http://` and `https://` are both accepted. SSRF controls apply; **content integrity** over cleartext HTTP does not (a network attacker can substitute a model). Prefer HTTPS sources. | Residual |
| **pause / resume** | Required `--confirm` as of 0.3.0, matching stop/print/delete/gcode. | Fixed |
| **Windows secret ACLs** | POSIX `0600` enforcement does not apply on Windows; protect the config directory with NTFS ACLs on shared machines. | Platform residual |
Expand Down
10 changes: 10 additions & 0 deletions bambu_cli/cliparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,16 @@ def build_parser():
"With --output: inserts the timestamp before the file extension."
),
)
p_snap.add_argument(
"--allow-camera-streamer",
action="store_true",
dest="allow_camera_streamer",
help=(
"Allow the unpinned Docker/RTSP camera streamer when the direct grab fails. "
"Required for X1-series. Also settable as camera_allow_streamer in config.json. "
"Ignored when camera_direct_only is set."
),
)

p_doc = sub.add_parser(
"doctor", parents=[get_global_parser()], help="Run health check and discover printer capabilities"
Expand Down
2 changes: 1 addition & 1 deletion bambu_cli/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
cmd_preflight,
cmd_setup,
cmd_slice,
cmd_snapshot,
)
from bambu_cli.commands.snapshot import cmd_snapshot # noqa: F401
from bambu_cli.commands.status import cmd_status # noqa: F401
from bambu_cli.commands.tui_cmd import cmd_tui # noqa: F401
19 changes: 13 additions & 6 deletions bambu_cli/commands/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,13 +203,20 @@ def shown_ip():
"chamber_light": True,
"camera_snapshot": ctx.settings.printer_model in _DIRECT_CAMERA_MODELS,
"camera_snapshot_note": (
"P1P/P1S/A1/A1M capture directly from the printer camera and need no Docker; "
"X1-series fall back to the optional BambuP1Streamer container"
if not ctx.settings.camera_direct_only
"camera_direct_only is set: only the direct printer-camera grab is used "
"(P1P/P1S/A1/A1M). The BambuP1Streamer is refused, so X1-series "
"snapshots are unavailable until the option is unset and "
"camera_allow_streamer is set"
if ctx.settings.camera_direct_only
else (
"camera_direct_only is set: only the direct printer-camera grab is used "
"(P1P/P1S/A1/A1M). The BambuP1Streamer fallback is refused, so X1-series "
"snapshots are unavailable until the option is unset"
"P1P/P1S/A1/A1M capture directly from the printer camera and need no Docker; "
"X1-series need camera_allow_streamer (or --allow-camera-streamer) to use "
"the optional BambuP1Streamer container"
if not ctx.settings.camera_allow_streamer
else (
"P1P/P1S/A1/A1M capture directly; camera_allow_streamer is set so "
"X1-series can use the unpinned BambuP1Streamer if the direct grab fails"
)
)
),
},
Expand Down
14 changes: 0 additions & 14 deletions bambu_cli/commands/setup_wrappers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
"""Thin command wrappers that delegate to focused packages."""

from bambu_cli.context import RuntimeContext


def cmd_setup(args):
"""Interactive or non-interactive printer configuration setup."""
Expand Down Expand Up @@ -34,18 +32,6 @@ def cmd_slice(args, **collaborators):
return _cmd_slice(args)


def cmd_snapshot(args, ctx=None, **collaborators):
"""Capture a camera snapshot using the RTSP Streamer Docker container.

Extra keyword args are forwarded to ``camera._cmd_snapshot`` (injectable
collaborators: grab_frame, which, subprocess_run, access_code_loader, …).
"""
from bambu_cli.protocols.camera import _cmd_snapshot

ctx = ctx or RuntimeContext.for_request(args)
_cmd_snapshot(args, ctx=ctx, **collaborators)


def cmd_preflight(args):
"""Check local install/config readiness without contacting printer."""
from bambu_cli.setup_cmd import _cmd_preflight
Expand Down
Loading
Loading