Files
continuum-proxy/Cargo.toml
T
octoturgeandClaude Opus 5 48672b1f45 Execute start_print, and never await it on the uplink task
The gateway can now run a cloud-dispatched print: download the job file from
the presigned URL in the command, then hand it to the existing
`upload_project_file`, which derives the filament mapping from the archive,
transfers it over FTPS and starts it.

## The download refuses to be a weapon

`src/uplink/download.rs` checks the HTTP status before reading a byte,
because an expired presigned URL answers 403 with an XML error document and
writing *that* to a printer as a project is the worst outcome available. It
rejects a `Content-Length` over 256 MiB before reading, and enforces the cap
while streaming as well, so a server that lies or omits the length cannot
make a small gateway box buffer until the OOM killer arrives. A byte count
that disagrees with `Content-Length` is an error: a `.3mf` is a zip, and a
truncated one loses the central directory the printer needs. HTTPS only, no
redirects, and the URL is stripped from error messages.

`PrinterCommand` no longer derives `Debug` for the same reason - `dispatch`
logs every command, and a presigned URL is a bearer credential until it
expires. It prints as `<redacted>`.

## Why it is spawned

`start_print` is the one command that cannot be answered inline, and the
reason is not latency. Everything else here is an MQTT publish returning in
milliseconds. This downloads tens of megabytes and then transfers them at a
measured 0.2 MiB/s - about four minutes for a 50 MB project - and awaiting
that inside the uplink's `select!` loop stops heartbeats and, far worse,
stops telemetry for the duration.

No telemetry is what makes spawning mandatory rather than tidy. The cloud now
derives a job's state from the telemetry that follows a start, so a gateway
that goes silent for four minutes during every job start withholds exactly
the information the job depends on - while the backend times the command out
after ten seconds and concludes the start failed. The job would be requeued
onto a printer that was busy printing it.

So the `ok` means "accepted, working on it", which is all a start reply was
ever trusted to mean. `runs_for_minutes` is an exhaustive match rather than a
`matches!`, so a new variant has to make that choice instead of defaulting to
the stall.

The gap this leaves is honest and recorded in the code: a failure after the
ack has no `CommandResult` to travel on, because answering later needs an
outbound channel this loop does not have. Until it does, the gateway's log is
where the reason lives and the backend reaps the job.

reqwest 0.12 with `rustls-tls-native-roots`, not 0.13: 0.13 pulls
`webpki-root-certs` under CDLA-Permissive-2.0, which is not on the allow
list, and `aws-lc` brings OpenSSL-licensed C. `cargo deny check licenses`
passes. 54 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 22:53:47 +00:00

84 lines
3.0 KiB
TOML

[package]
name = "continuum-proxy"
version = "0.1.0"
edition = "2024"
description = "Continuum edge gateway daemon - runs on-site, bridges printers to the cloud control plane"
# Proprietary: deliberately not an SPDX id, so cargo-deny treats it as
# first-party. See docs/LICENSES.md in continuum-common.
license = "LicenseRef-Proprietary"
publish = false
[lib]
name = "continuum_proxy"
path = "src/lib.rs"
[[bin]]
name = "continuum-proxy"
path = "src/main.rs"
[[example]]
name = "printer_polymorphism"
path = "examples/printer_polymorphism.rs"
[[example]]
name = "test_bambu_certs"
path = "examples/test_bambu_certs.rs"
[[example]]
name = "auto_connect_bambu"
path = "examples/auto_connect_bambu.rs"
[[example]]
name = "bambu_commands_demo"
path = "examples/bambu_commands_demo.rs"
[[example]]
name = "live_reports"
path = "examples/live_reports.rs"
[[example]]
name = "bambu_idle_checks"
path = "examples/bambu_idle_checks.rs"
[dependencies]
# Real printer protocol clients (MQTT for Bambu, HTTP for PrusaLink/
# Moonraker, FTPS for gcode transfer) go here once you're ready to build
# past the stubs in src/printer/ - see that module's doc comment.
continuum-rust-types = { path = "../continuum-schemas/packages/rust-types" }
# FOR NON-LOCAL DEV
# continuum-rust-types = { git = "https://git.octoturge.com/Continuum/continuum-schemas.git" }
# Builds a JSON instance straight from the schema files in
# continuum-schemas/schemas/ - used for Bambu's raw MQTT JSON messages
# instead of hand-written serde structs (see bambu.rs).
continuum-schema-instance = { path = "../continuum-common/packages/rust-schema-instance" }
tokio = { version = "1.40", features = ["full"] }
tokio-tungstenite = { version = "0.24", features = ["native-tls"] }
futures-util = "0.3"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
toml = "0.8"
native-tls = "0.2"
base64 = "0.22"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
anyhow = "1"
thiserror = "1"
dotenvy = "0.15"
tokio-serial = { version = "5.5.0", default-features = false }
rumqttc = { version = "0.25.1", default-features = false, features = ["use-native-tls"] }
# Blocking FTPS client for pushing project files to a printer's own
# storage (see src/printer/bambu_ftps.rs for why the blocking one, not
# the async one). `deprecated` is what gates `connect_secure_implicit`:
# Bambu speaks implicit FTPS on 990, which the RFCs deprecated in favour
# of explicit AUTH TLS. The printers do not care.
# MIT OR Apache-2.0 - checked against docs/LICENSES.md before adding.
suppaftp = { version = "12.1.1", features = ["native-tls", "deprecated"] }
zip = { version = "9.0.3", default-features = false, features = ["deflate-flate2"] }
flate2 = "1.1.10"
# HTTPS GET of a presigned job-file URL for the `start_print` command (see
# src/uplink/download.rs). Features are the bare minimum: TLS via rustls, no
# openssl-sys; no json/gzip/cookies/etc, since the body is an opaque binary.
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-native-roots"] }