Diagram — Job Signing Sequence¶
Referenced from docs/technical/ARCHITECTURE.md. Covers both dispatch paths from sign to report.
sequenceDiagram
actor Op as Operator CLI
participant Signer as JobSigner
participant Inbox as OneDrive inbox
participant Queue as QueueService<br/>(target's own listen/serve)
participant Gate as AvailabilityGate
participant Governor as GovernorGate
participant Exec as IJobExecutor
participant Outbox as OneDrive outbox
participant Peer as DispatchServer /dispatch<br/>(target's own serve)
rect rgb(235, 245, 255)
note over Op,Outbox: Path A — enqueue (async, OneDrive-queued)
Op->>Signer: build DispatchJob, CanonicalString v2 (…|jobClass)
Signer->>Op: HMAC-SHA256(key, canonical) → Sig
Op->>Inbox: atomic write {id}.json (temp file + File.Move)
Queue->>Inbox: poll, read oldest-first
Queue->>Queue: SchemaVersion <= CurrentSchemaVersion?
Queue->>Queue: JobSigner.Verify (FixedTimeEquals)
Queue->>Queue: job id shape, HarnessRegistry.IsKnown(kind)
Queue->>Queue: replay check (done/outbox already has this id?)
Queue->>Queue: wrong-target / cancel-before-start check
Queue->>Gate: AvailabilityGate.Evaluate(jobClass, freshSignals, now)
alt refused
Gate-->>Queue: Refused — reason, blockLevel
Queue->>Inbox: job file left untouched (requeue next poll)
else allowed
Queue->>Governor: ShouldDefer(tier, critical)?
alt RED/BLACKOUT and non-critical
Governor-->>Queue: defer
Queue->>Inbox: job file left untouched (requeue next poll)
else run it
Queue->>Queue: move inbox → processing
Queue->>Exec: ExecuteAsync(job)
Exec-->>Queue: DispatchResult
Queue->>Outbox: atomic write {id}.json
Queue->>Queue: move processing → archive/done
end
end
end
rect rgb(255, 245, 235)
note over Op,Peer: Path B — send (sync, direct LAN peer)
Op->>Signer: build DispatchJob, sign (same canonical)
Op->>Peer: POST /dispatch, X-Fleet-Auth: sha256(key) hex
Peer->>Peer: VerifyBearer (FixedTimeEquals) — before reading body
Peer->>Peer: read body (max 256KB)
Peer->>Peer: SchemaVersion check → 422 if too new
Peer->>Peer: JobSigner.Verify → 401 if invalid
Peer->>Peer: job.Target == this PC? → 400 if not
Peer->>Peer: replay (done/outbox) → 409 if seen
Peer->>Peer: cancel-before-start marker? → 200 pre-cancelled if so
Peer->>Gate: AvailabilityGate.Evaluate(jobClass, freshSignals, now)
alt refused
Gate-->>Peer: Refused
Peer-->>Op: 503 {deferred:true, unavailable:true, reason, blockLevel}
else allowed
Peer->>Governor: ShouldDefer(tier, critical)?
alt RED/BLACKOUT and non-critical
Governor-->>Peer: defer
Peer-->>Op: 503 {deferred:true, tier}
else run it
Peer->>Peer: InFlight.TryAdd(id) → 409 if already running
Peer->>Exec: ExecuteAsync(job) under job.TimeoutSec
Exec-->>Peer: DispatchResult
Peer->>Outbox: atomic write {id}.json
Peer-->>Op: 200 DispatchResult (same request, synchronous)
end
end
end
Both paths share the exact same verify order (schema → HMAC → job-id shape → kind → replay →
target → cancel → availability gate → governor gate) and the exact same IJobExecutor — the only
difference is transport (file vs. HTTP) and whether the caller waits in the same request.