Skip to content

feat: submission pipeline up to the approval gate - #2

Merged
cyrilcaoyang merged 1 commit into
mainfrom
feat/submission-pipeline
Sep 7, 2026
Merged

feat: submission pipeline up to the approval gate#2
cyrilcaoyang merged 1 commit into
mainfrom
feat/submission-pipeline

Conversation

@cyrilcaoyang

Copy link
Copy Markdown
Collaborator

Implements docs/SUBMISSION_PIPELINE_DESIGN.md: remote users submit a print artifact, it is validated against the target machine's profile, and valid jobs wait in a per-machine queue with expected finish times.

Dispatch is deliberately not built. submissions.dispatch() raises DispatchUnavailable, no route calls it, and no route can enter dispatching / running / finished. There are still no /control/* routes and the service issues no printer commands. Everything shipped here is read-and-analysis: it reads the monitor's cache and writes only to the gateway's own disk.

New modules

file owns
profiles.py Machine profile — operator-declared config merged with live telemetry, each field tagged observed / declared / unknown
artifacts.py Read-only inspection of .3mf and .gcode
validation.py The eight checks from the design's §6
submissions.py Intake, job state machine, durable store, dispatch stub
queueing.py Per-machine queue and expected finish times

Routes: GET /printers/{id}/profile, GET /printers/{id}/queue, POST /submissions, GET /submissions[/{id}], POST /submissions/{id}/approve.

Decisions on the design's §12 open blockers

All follow the design's own recommendation, and none changes what ships while dispatch is stubbed. Recorded in docs/TODO.md.

  1. Approval — human-in-the-loop. Nothing auto-approves.
  2. Identity — opaque. ac_auth is not wired here, so requested_by / approved_by are opaque strings recorded in job history. Documented as not authentication.
  3. Queue — gateway-owned. Running remaining time from printer telemetry, queued durations from the artifact's own slicer estimate.
  4. .3mf parsing — standard library. No new dependency beyond python-multipart for the upload form.

Two judgement calls worth reviewing

  • No mesh parsing. A printable .3mf is a sliced plate file embedding Metadata/plate_N.gcode, so the footprint comes from scanning that rather than 3D/3dmodel.model. This sidesteps the 3MF build-transform coordinate ambiguity and makes "this project file is not sliced, no printer can run it" a real check. The footprint is an extent, so slicer placement cannot change the answer.
  • not_applicable is a third check outcome, never a pass. The design's §10 data gaps (no AMS trays on either live printer, blank H2D nozzle type) report "not compared, and why" instead of quietly approving. Undeclared bed_size_mm / limits behave the same way — there are no built-in defaults, because a guessed limit is a fabricated machine fact.

Safety of untrusted input

Size caps on upload and scan; a truncated scan withholds the footprint rather than reporting a partial one as whole; artifacts stored under the submission's UUID, never a client path; no response carries a stored path; XML with a document type declaration is refused outright, since that is the only place an entity expansion can be declared.

Note for the reviewer

This branch also carries the previously uncommitted rich read-only telemetry work already recorded as done in docs/TODO.md. It touches the same structures (AmsTrayReading, monitor.status() details, the same test fixtures) and could not be split out cleanly after the fact. It is live on the deployed service today but was absent from main.

After merging

  • printers.local.yaml has no profile: blocks on either printer. Until they are added the profile-dependent checks abstain with not_applicable. Declare nozzle_type on the H2D, whose live field is blank.
  • The deployed service runs from the working tree, so the pipeline goes live on the next restart of bambu-server.
  • The service binds 127.0.0.1:8012 with no proxy route, so /submissions is not reachable off-host yet.

uv run ruff check . and uv run pytest -q (114 tests) pass. Tests build their own .3mf / .gcode fixtures and use fake backends; nothing touches hardware.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UQvsfEeDitEyNzbCwrcEdD

Implements docs/SUBMISSION_PIPELINE_DESIGN.md: remote users submit a print
artifact, it is validated against the target machine's profile, and valid jobs
wait in a per-machine queue with expected finish times.

Dispatch is deliberately NOT built. `submissions.dispatch()` raises
`DispatchUnavailable`, no route calls it, and no route can enter
`dispatching` / `running` / `finished`. There are still no `/control/*` routes
and the service issues no printer commands. The whole shipped path is
read-and-analysis: it reads the monitor's cache and writes only to the
gateway's own disk.

New modules:

- `profiles.py`   machine profile: operator-declared config merged with live
                  telemetry, each field tagged observed / declared / unknown
- `artifacts.py`  read-only inspection of .3mf and .gcode
- `validation.py` the eight checks from the design's §6
- `submissions.py` intake, job state machine, durable store, dispatch stub
- `queueing.py`   per-machine queue and expected finish times

Routes: GET /printers/{id}/profile, GET /printers/{id}/queue,
POST /submissions, GET /submissions[/{id}],
POST /submissions/{id}/approve.

Decisions on the design's §12 open blockers, all following its own
recommendation and none of which changes what ships while dispatch is stubbed:
human-in-the-loop approval; opaque requested_by / approved_by (documented as
not authentication); gateway-owned queue; standard-library .3mf parsing.
Recorded in docs/TODO.md.

Two judgement calls worth recording:

- No mesh parsing. A printable .3mf is a *sliced* plate file embedding
  Metadata/plate_N.gcode, so the footprint comes from scanning that rather than
  3D/3dmodel.model. This sidesteps the 3MF build-transform coordinate
  ambiguity, and makes "this project file is not sliced, no printer can run it"
  a real check. The footprint is an extent, so slicer placement cannot change
  the answer.
- `not_applicable` is a third check outcome, never a pass. The design's §10
  data gaps (no AMS trays on either live printer, blank H2D nozzle type) report
  "not compared, and why" instead of quietly approving. Undeclared bed_size_mm
  and limits behave the same way: there are no built-in defaults, because a
  guessed limit is a fabricated machine fact.

Untrusted input is bounded throughout: size caps on upload and scan, a
truncated scan withholds the footprint rather than reporting a partial one as
whole, artifacts are stored under the submission's UUID (never a client path),
no response carries a stored path, and XML with a document type declaration is
refused outright since that is the only place an entity expansion can be
declared.

The backend also now reads each AMS tray's nozzle_temp_min / nozzle_temp_max,
so the filament-window check becomes real as soon as tray data appears.

This commit also carries the previously uncommitted rich read-only telemetry
work already recorded as done in docs/TODO.md; the two files touch the same
structures and could not be separated cleanly after the fact.

`uv run ruff check .` and `uv run pytest -q` (114 tests) pass. Tests build
their own .3mf and .gcode fixtures and use fake backends; nothing touches
hardware.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UQvsfEeDitEyNzbCwrcEdD
@cyrilcaoyang
cyrilcaoyang merged commit 82c768c into main Sep 7, 2026
1 check passed
@cyrilcaoyang
cyrilcaoyang deleted the feat/submission-pipeline branch September 7, 2026 02:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants