Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ea8eb0d
Model reported printer setup and prepare Altra fit gauges
Oct 3, 2026
ade2f6a
Route printer jobs through ntfy-approved workload identity
Oct 3, 2026
a82c5f3
Enable verified gauge slicing and ntfy-approved operator printing
Oct 3, 2026
0da75ef
Route approval submissions to the active store writer
Oct 3, 2026
243f587
Select confirmed AMS slot one for both printer gauges
Oct 3, 2026
65d6bef
Wait for complete printer telemetry before start admission
Oct 3, 2026
9c2c012
Request full printer status before admitting gauge starts
Oct 3, 2026
f48bdc6
Record confirmed printer one gauge execution and printer two storage …
Oct 3, 2026
ec1a458
Record gauge fit results and prepare remaining print candidates
Oct 3, 2026
97b0366
Stop further gauge queue and record chassis prototype direction
Oct 3, 2026
c3fc919
Record decision to finish the active gauge only
Oct 3, 2026
2d91227
Record hosted interactive chassis concept workbench
Oct 3, 2026
e1bccf9
Record interchangeable PSU and cassette airflow concept
Oct 4, 2026
378706a
Adopt shared fixed-frame power per four-node chassis block
Oct 4, 2026
bcffcf4
Prepare rear exhaust fan-mount prototype through modeled CAD and slicing
Oct 4, 2026
6b06777
Record approved rear fan-plate start and live RUNNING observation
Oct 4, 2026
c1f6d42
Model complete cassette assembly and publish generated CAD review
Oct 4, 2026
08506e8
Route approval clients to the active store writer
Oct 4, 2026
a3ffd78
Merge main and unify printer starts behind approval and durable claims
Oct 4, 2026
4f0cfd7
Separate cassette CAD and fit artifacts from the printer path
Oct 4, 2026
cdf6f43
Merge separately reviewed approval-client cutover prerequisite
Oct 4, 2026
8fc65ac
Classify attended print requests and separate CAD and slicer scopes
Oct 4, 2026
4aa8963
Regenerate printer workflow with its required release artifact shape
Oct 4, 2026
6da087d
Import printer setup-report algebra providers explicitly
Oct 4, 2026
54bd96e
Use vendor job readiness and classify spurious Bambu 0500C011
Oct 4, 2026
a96a2fd
Reconcile printer starts automatically against exact-job telemetry
Oct 4, 2026
46bdc0a
Merge main and preserve printer and workspace workflow modes
Oct 4, 2026
7be00dc
Merge latest main Spark dispatch changes and regenerate fleet workflow
Oct 4, 2026
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
181 changes: 172 additions & 9 deletions .github/workflows/fleet-converge.yml

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions dag/extdeps/exec/command.dag
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@ fn argv_command(program: ProgramIdentity, arguments: List<String>) -> ArgvComman
decl_ref(module_path: "extdeps.tools.nbdkit", decl_name: "nbdkit_readonly_file_serve_command"),
decl_ref(module_path: "extdeps.tools.websocat", decl_name: "websocat_nbd_proxy_bridge_command"),
decl_ref(module_path: "extdeps.tools.mosquitto_pub", decl_name: "mosquitto_publish_command"),
decl_ref(module_path: "extdeps.tools.mosquitto_pub", decl_name: "mosquitto_pub_help_command"),
decl_ref(module_path: "extdeps.tools.mosquitto_sub", decl_name: "mosquitto_report_command"),
decl_ref(module_path: "extdeps.tools.mosquitto_rr", decl_name: "mosquitto_snapshot_command"),
decl_ref(module_path: "extdeps.tools.gnu_time", decl_name: "gnu_time_max_rss_command"),
decl_ref(module_path: "extdeps.systemd.systemctl", decl_name: "systemctl_version_command"),
decl_ref(module_path: "extdeps.systemd.systemctl", decl_name: "systemctl_user_manager_probe_command"),
Expand Down
2 changes: 2 additions & 0 deletions dag/extdeps/exec/program.dag
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ fn cataloged_program(invocation: NonEmptyStr) -> ProgramIdentity
decl_ref(module_path: "extdeps.tools.sed", decl_name: "sed_program"),
decl_ref(module_path: "extdeps.tools.conntrack", decl_name: "conntrack_program"),
decl_ref(module_path: "extdeps.tools.mosquitto_pub", decl_name: "mosquitto_pub_program"),
decl_ref(module_path: "extdeps.tools.mosquitto_sub", decl_name: "mosquitto_sub_program"),
decl_ref(module_path: "extdeps.tools.mosquitto_rr", decl_name: "mosquitto_rr_program"),
decl_ref(module_path: "extdeps.tools.busybox", decl_name: "busybox_httpd_command"),
decl_ref(module_path: "extdeps.tools.busybox", decl_name: "busybox_tftpd_over_udpsvd_command"),
decl_ref(module_path: "extdeps.tools.busybox", decl_name: "busybox_dd_sparse_image_command"),
Expand Down
30 changes: 25 additions & 5 deletions dag/extdeps/printing/bambu_lab_lan_protocol.dag
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import std.decl_ref { DeclarationRef, WholeDeclaration }
import extdeps.external_authority { ExternalAuthority, ExternalModelScope, ExternalSubjectRef }
import extdeps.uri { Uri, Https }
import extdeps.oasis.mqtt { MqttEndpoint, MqttTopic, MqttOverTls, MqttQos, ExactlyOnce, AtLeastOnce }
import extdeps.languages.json.emit { JsonValue, JsonKeyValue, json_kv, json_object, json_string, json_bool, serialize_json }
import extdeps.languages.json.emit { JsonValue, JsonKeyValue, json_kv, json_object, json_string, json_bool, json_array, json_int, serialize_json }

// THE LAN CONTROL PROTOCOL OF BAMBU LAB PRINTERS IN LAN-ONLY MODE, modeled because the delivery leg
// already speaks FTPS and the leg that STARTS a job speaks MQTT -- and that second leg was
Expand Down Expand Up @@ -69,9 +69,8 @@ fn bambu_lan_mqtt_endpoint(host: NonEmptyStr) -> MqttEndpoint {
// meaningless unless delivery has completed, and the sequencing is a real dependency rather than a
// convention.
//
// use_ams IS FALSE BECAUSE THESE MACHINES HAVE NO AMS BOUND TO THIS PRINT. The operator's constraint
// is one filament per printer -- printer-01 white, printer-02 black -- which is exactly what the
// design wants, since the logo is a slot-in insert rather than a multi-colour feature.
// Operator confirmed AMS physical slot 1 on both printers on 2026-10-03.
// The single project filament maps to zero-indexed tray 0.
type BambuPrintStartCommand {
file_url: NonEmptyStr
plate_gcode: NonEmptyStr
Expand All @@ -96,7 +95,7 @@ fn bambu_start_command(remote_name: NonEmptyStr) -> BambuPrintStartCommand {
file_url: bambu_sd_file_url(remote_name: remote_name),
plate_gcode: bambu_first_plate_gcode,
subtask_name: remote_name,
use_ams: false,
use_ams: true,
bed_leveling: true,
}
}
Expand Down Expand Up @@ -155,6 +154,7 @@ fn bambu_start_payload_json(command: BambuPrintStartCommand, sequence_id: NonEmp
json_kv(key: "url", value: json_string(s: command.file_url as String)),
json_kv(key: "subtask_name", value: json_string(s: command.subtask_name as String)),
json_kv(key: "use_ams", value: json_bool(b: command.use_ams)),
json_kv(key: "ams_mapping", value: json_array(elements: [json_int(n: 0)])),
json_kv(key: "bed_leveling", value: json_bool(b: command.bed_leveling)),
json_kv(key: "flow_cali", value: json_bool(b: false)),
json_kv(key: "vibration_cali", value: json_bool(b: true)),
Expand Down Expand Up @@ -204,3 +204,23 @@ data bambu_printer_ca_bundle_source: ExternalAuthority = ExternalAuthority {
data bambu_printer_ca_bundle_sha256: NonEmptyStr = "36f2bcee347ec7adce719b5fd350099591a4d3d0ec4e039c7019890d78e152a0"

data bambu_ca_measured_fingerprint_sha256: NonEmptyStr = "030BCA81CECE18B7EFF3CFD2B75D09D3EFCA893BC069609E37FA04257FE4D840"

fn bambu_status_snapshot_payload() -> String {
serialize_json(v: json_object(members: [json_kv(key: "pushing", value: json_object(members: [
json_kv(key: "command", value: json_string(s: "pushall")),
json_kv(key: "sequence_id", value: json_string(s: "0")),
json_kv(key: "version", value: json_int(n: 1)),
json_kv(key: "push_target", value: json_int(n: 1)),
]))]))
}

// Bambu Studio MachineObject::can_print accepts IDLE and FINISH; FINISH is
// excluded from is_in_printing_status. This is job readiness, never bed-clear evidence.
// https://github.com/bambulab/BambuStudio/blob/master/src/slic3r/GUI/DeviceManager.cpp
fn bambu_completed_or_idle_state(state:String) -> Bool {state=="IDLE"||state=="FINISH"}

// 83935249 is 0x0500C011. Bambu's collaborator calls this a non-existent error
// that should be deleted; the vendor error table has a blank description for it.
// https://github.com/bambulab/BambuStudio/issues/4495#issuecomment-2275068828
// This exact wire value is classified, not all nonzero codes or all blank UI messages.
fn bambu_start_error_nonblocking(code:String) -> Bool {code=="0"||code=="83935249"}
37 changes: 37 additions & 0 deletions dag/extdeps/tools/mosquitto_pub.dag
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ module extdeps.tools.mosquitto_pub

import extdeps.exec.program { ProgramIdentity, cataloged_program }

import extdeps.shell.exec { ProcessObservation }

import std.types { String, NonEmptyStr, Bool, Port, FilePath, List }
import std.decl_ref { DeclarationRef, WholeDeclaration }
import extdeps.version { VersionConstraint }
Expand Down Expand Up @@ -123,3 +125,38 @@ fn mosquitto_publish_command(
],
)
}

// The upstream synopsis exposes --help, not --version. Its version banner may follow
// the description line; publishing is never used as a tool-availability probe.
fn mosquitto_pub_help_command() -> ArgvCommand {
argv_command(program: mosquitto_pub_program(), arguments: ["--help"])
}

fn mosquitto_pub_help_supports_private_options(stdout: String) -> Bool {
any(split(s: stdout, delimiter: "\n"), line => mosquitto_pub_version_line_supported(stdout: line))
}

fn mosquitto_pub_version_line_supported(stdout: String) -> Bool {
let prefix = "mosquitto_pub version "
if !starts_with(s: stdout, prefix: prefix) { false }
else {
match split(s: stdout, delimiter: " ") |> skip(n: 2) |> first {
Absent => false
Present { value: version } =>
match split(s: version, delimiter: ".") |> first {
Absent => false
Present { value: major } => match parse_int(s: major) {
Absent => false
Present { value: n } => n >= 2
}
}
}
}
}

// Measured on the installed 2.0.18 client: successful --help prints its banner but exits 1.
// Code 1 without that specific supported-version banner is still a refusal.
fn mosquitto_pub_help_observation_admitted(observation: ProcessObservation) -> Bool {
(observation.exit_code == 0 || observation.exit_code == 1) &&
mosquitto_pub_help_supports_private_options(stdout: observation.stdout)
}
33 changes: 33 additions & 0 deletions dag/extdeps/tools/mosquitto_rr.dag
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
module extdeps.tools.mosquitto_rr

import std.types { NonEmptyStr, String, FilePath }
import extdeps.exec.command { ArgvCommand, argv_command }
import extdeps.exec.program { ProgramIdentity, cataloged_program }
import extdeps.tools { CliTool, SourceApt }
import extdeps.apt { package_mosquitto_clients, apt_bin_dir }
import extdeps.tools.mosquitto_pub { mosquitto_pub_pinned_version }
import extdeps.oasis.mqtt { MqttEndpoint, MqttTopic }
import extdeps.external_authority { ExternalAuthority }
import extdeps.uri { Uri, Https }

data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority {
uri: Uri { scheme: Https, locator: "mosquitto.org/man/mosquitto_rr-1.html" }
}
fn mosquitto_rr_program() -> ProgramIdentity = cataloged_program(invocation: "mosquitto_rr")

data mosquitto_rr_cli_tool: CliTool = CliTool {
program: mosquitto_rr_program(),
min_version: Present { value: mosquitto_pub_pinned_version },
installable_via: [SourceApt { package: package_mosquitto_clients, bin_dir: apt_bin_dir }],
}

data mosquitto_rr_options_file_name: NonEmptyStr = "mosquitto_rr"

// Subscribe before requesting read-only status, using MQTT 3.1.1 and a bounded wait.
fn mosquitto_snapshot_command(endpoint: MqttEndpoint, topic: MqttTopic, request_topic: MqttTopic, payload: String, ca_bundle: FilePath) -> ArgvCommand {
argv_command(program: mosquitto_rr_program(), arguments: [
"-h", endpoint.host as String, "-p", to_string(endpoint.port),
"--cafile", ca_bundle as String, "--insecure", "-e", topic as String,
"-t", request_topic as String, "-m", payload, "-V", "mqttv311", "-q", "1", "-R", "-W", "15",
])
}
34 changes: 34 additions & 0 deletions dag/extdeps/tools/mosquitto_sub.dag
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
module extdeps.tools.mosquitto_sub

import std.types { NonEmptyStr, String, FilePath }
import extdeps.exec.command { ArgvCommand, argv_command }
import extdeps.exec.program { ProgramIdentity, cataloged_program }
import extdeps.tools { CliTool, SourceApt }
import extdeps.apt { package_mosquitto_clients, apt_bin_dir }
import extdeps.tools.mosquitto_pub { mosquitto_pub_pinned_version }
import extdeps.oasis.mqtt { MqttEndpoint, MqttTopic }
import extdeps.external_authority { ExternalAuthority }
import extdeps.uri { Uri, Https }

data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority {
uri: Uri { scheme: Https, locator: "mosquitto.org/man/mosquitto_sub-1.html" }
}
fn mosquitto_sub_program() -> ProgramIdentity = cataloged_program(invocation: "mosquitto_sub")

data mosquitto_sub_cli_tool: CliTool = CliTool {
program: mosquitto_sub_program(),
min_version: Present { value: mosquitto_pub_pinned_version },
installable_via: [SourceApt { package: package_mosquitto_clients, bin_dir: apt_bin_dir }],
}

data mosquitto_sub_options_file_name: NonEmptyStr = "mosquitto_sub"

// One report, bounded by 15 seconds. TLS validates the same pinned issuer as the
// publisher; the machine serial selects the report topic. No command is published.
fn mosquitto_report_command(endpoint: MqttEndpoint, topic: MqttTopic, ca_bundle: FilePath) -> ArgvCommand {
argv_command(program: mosquitto_sub_program(), arguments: [
"-h", endpoint.host as String, "-p", to_string(endpoint.port),
"--cafile", ca_bundle as String, "--insecure", "-t", topic as String,
"-R", "-C", "1", "-W", "15",
])
}
24 changes: 18 additions & 6 deletions dag/gunbc/auth/approval_request_client.dag
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
module gunbc.auth.approval_request_client

import gunbc.auth.approval_cutover_posture { ApprovalCutoverPosture, approval_cutover_posture, RoadmapServesAndWrites, BrokerServesAndWrites, BrokerOwnsAlone }
import gunbc.auth.approval_broker_endpoint { approval_broker_loopback_base_url, approval_broker_listen_port, approval_broker_submit_path }

import std.types { Bool, NonEmptyStr, String, Timestamp, Secret }
import std.scoped_authorization { AuthorizationRequest }
import extdeps.crypto.mac {
Expand All @@ -18,14 +21,23 @@ import gunbc.auth.approval_request_submission {
import gunbc.fleet_posix_accounts { fleet_operator_email }
import gunbc.roadmap_dashboard_instance { srv1_live_dashboard_instance }

data approval_request_submit_path: NonEmptyStr = "/approvals"
data approval_request_submit_path: NonEmptyStr = approval_broker_submit_path

// Local submissions must follow the same cutover posture as writer authority. The
// roadmap endpoint deliberately refuses store writes after the broker takes ownership.
fn approval_loopback_origin_for(posture: ApprovalCutoverPosture) -> NonEmptyStr {
match posture {
RoadmapServesAndWrites => {
let instance = srv1_live_dashboard_instance()
concat(concat("http://", instance.listen_host as String), concat(":", to_string(instance.listen_port))) as NonEmptyStr
}
BrokerServesAndWrites => approval_broker_loopback_base_url(port: approval_broker_listen_port)
BrokerOwnsAlone => approval_broker_loopback_base_url(port: approval_broker_listen_port)
}
}

fn approval_loopback_origin() -> NonEmptyStr {
let instance = srv1_live_dashboard_instance()
concat(
concat("http://", instance.listen_host as String),
concat(":", to_string(instance.listen_port)),
) as NonEmptyStr
approval_loopback_origin_for(posture: approval_cutover_posture)
}

fn approval_submit_url() -> NonEmptyStr {
Expand Down
5 changes: 4 additions & 1 deletion dag/gunbc/auth/authorization_pattern_selection.dag
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import std.decision {
select_realization,
}
import std.human_intervention {
HumanIntervention, HumanInterventionFrequency, FleetOnce, DeviceOnce, EveryProvision, BreakGlassOnly,
HumanIntervention, HumanInterventionFrequency, FleetOnce, DeviceOnce, EveryProvision, EveryOperatorRequest, BreakGlassOnly,
DischargedAt, NoDischargeSiteModeled,
}
import extdeps.external_authority { ExternalAuthority, CitedToAuthority }
Expand Down Expand Up @@ -161,6 +161,7 @@ fn witness_required(e: PrivilegedEffect) -> Bool {
BreakGlassOnly => true
FleetOnce => false
DeviceOnce => false
EveryOperatorRequest => false
EveryProvision => false
}
irreversible_undischarged || wide || bills || break_glass
Expand All @@ -171,6 +172,7 @@ fn witness_required(e: PrivilegedEffect) -> Bool {
// operator learns to tap without reading.
fn unattended_required(e: PrivilegedEffect) -> Bool {
match e.frequency {
EveryOperatorRequest => false
EveryProvision => true
FleetOnce => false
DeviceOnce => false
Expand Down Expand Up @@ -512,6 +514,7 @@ fn frequency_text(f: HumanInterventionFrequency) -> String {
match f {
FleetOnce => "FleetOnce"
DeviceOnce => "DeviceOnce"
EveryOperatorRequest => "EveryOperatorRequest"
EveryProvision => "EveryProvision"
BreakGlassOnly => "BreakGlassOnly"
}
Expand Down
3 changes: 2 additions & 1 deletion dag/gunbc/auth/gcp_iam_converge.dag
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import gunbc.auth.heal_publisher_provision {
heal_publisher_dedicated_federation, private_heal_publisher_dedicated_federation, impersonation_cell, observe_and_classify,
rest_ok, rest_refusal_text, outcome_is_not_found,
}
import gunbc.fleet.printer_federation_provision { printer_dedicated_federation }
import gunbc.namecheap.federation_provision { namecheap_dedicated_federation }
import gunbc.auth.managed_host_boot_federation_provision { boot_dedicated_federation }
import gunbc.auth.managed_host_boot_federation { pinned_version_accessor_grant_for, provisioned_boot_federations }
Expand Down Expand Up @@ -215,7 +216,7 @@ data gcp_iam_converge_targets: List<DedicatedFederation> = concat(
[heal_publisher_dedicated_federation(), private_heal_publisher_dedicated_federation()],
map(provisioned_boot_federations(), f => boot_dedicated_federation(f: f)),
),
[namecheap_dedicated_federation()],
[namecheap_dedicated_federation(), printer_dedicated_federation()],
)

fn service_account_resource(email: ServiceAccountEmail) -> String {
Expand Down
44 changes: 23 additions & 21 deletions dag/gunbc/auth/mosquitto_options_binding.dag
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
module gunbc.auth.mosquitto_options_binding

import extdeps.tools.mosquitto_rr { mosquitto_rr_options_file_name }

import extdeps.tools.mosquitto_sub { mosquitto_sub_options_file_name }

import std.types { NonEmptyStr, String, Bool, Secret, FilePath, List }
import extdeps.shell
import extdeps.filesystem.filesystem_io { Filesystem }
Expand Down Expand Up @@ -93,27 +97,7 @@ fn place_mosquitto_options_privately(
password: Secret,
) -> MosquittoOptionsPlacement
{
let created = shell.Mktemp.Dir()
if created.success == false {
MosquittoOptionsRefused {
cause: "no private directory could be created for the credential, so none was placed",
}
} else {
let options_path = concat(
concat(created.path as String, "/"),
mosquitto_pub_options_file_name as String)
if Filesystem.WriteOwnerOnly(
path: options_path,
content: mosquitto_options_content(login: login, password: password)
).success {
MosquittoOptionsPlaced {
binding: MosquittoOptionsBinding { dir: created.path as FilePath },
}
} else {
mosquitto_options_write_refusal(
removed_after_failure: shell.Remove.RecursiveForce(path: created.path as FilePath).success)
}
}
place_mosquitto_named_options(login:login,password:password,filename:mosquitto_pub_options_file_name)
}

// SHREDDING IS UNCONDITIONAL AT THE CALL SITE, INCLUDING ON THE FAILING ARM. A refused publish that
Expand All @@ -124,3 +108,21 @@ fn place_mosquitto_options_privately(
fn shred_mosquitto_options_binding(binding: MosquittoOptionsBinding) -> Bool {
shell.Remove.RecursiveForce(path: binding.dir).success
}

// All clients use one private placement/cleanup bracket; only the upstream filename varies.
fn place_mosquitto_named_options(login:NonEmptyStr,password:Secret,filename:NonEmptyStr) -> MosquittoOptionsPlacement {
let created=shell.Mktemp.Dir()
if !created.success {MosquittoOptionsRefused {cause:"Cannot create private options directory"}}
else {
let path=join([created.path as String,"/",filename as String],"")
if Filesystem.WriteOwnerOnly(path:path,content:mosquitto_options_content(login:login,password:password)).success {
MosquittoOptionsPlaced {binding:MosquittoOptionsBinding {dir:created.path as FilePath}}
} else {mosquitto_options_write_refusal(removed_after_failure:shell.Remove.RecursiveForce(path:created.path as FilePath).success)}
}
}
fn place_mosquitto_sub_options_privately(login:NonEmptyStr,password:Secret) -> MosquittoOptionsPlacement {
place_mosquitto_named_options(login:login,password:password,filename:mosquitto_sub_options_file_name)
}
fn place_mosquitto_rr_options_privately(login:NonEmptyStr,password:Secret) -> MosquittoOptionsPlacement {
place_mosquitto_named_options(login:login,password:password,filename:mosquitto_rr_options_file_name)
}
Loading
Loading