# AOT Printer Agent Technical Spec

Version: 1.0  
Status: Final implementation reference  
Primary implementation path: Native Go Windows Service Agent  
Supersedes: all QZ Tray, browser print station, and Electron print station drafts

## 1. Scope

This document is the final technical implementation reference for AOT printer routing.

The official implementation is:

- Go Windows Service Agent.
- Outbound HTTPS only from customer site to Laravel cloud.
- Claim/lease APIs for print job delivery.
- `network_raw` adapter first.
- `windows_spooler` adapter later.
- Clear `sent` vs `confirmed` print semantics.

The first production target is a LAN receipt printer reachable by raw TCP, normally port `9100`, receiving ESC/POS bytes.

## 2. Explicitly Prohibited Designs

Do not implement or depend on:

- QZ Tray.
- Browser print station.
- Electron print station.
- Browser JavaScript access to local printers.
- Inbound HTTP, WebSocket, or VPN access from cloud to customer LAN.
- Cloud-initiated connections into the restaurant/site network.

These approaches are cancelled for this project.

## 3. Architecture Decision

The system has two sides:

- Laravel cloud application.
- Native Windows service agent installed at the customer site.

The agent runs near the printers, polls the cloud over HTTPS, claims jobs assigned to its approved printers, sends bytes to the printer, then reports status back to Laravel.

The cloud never connects directly to the site network. All traffic starts from the agent.

## 4. First End-to-End Milestone

The first milestone is:

1. One approved agent exists in Laravel.
2. One `network_raw` printer is configured and owned by that agent.
3. One print job exists in Laravel with base64 encoded ESC/POS bytes.
4. The agent sends heartbeat successfully.
5. The agent claims the job through the lease API.
6. The agent opens TCP connection to the printer IP and port.
7. The agent writes the decoded bytes.
8. Laravel receives status `sent`.
9. Status `confirmed` is reported only if the printer or adapter can prove physical acceptance beyond socket write.

For MVP, `sent` is the success status after bytes are written to the TCP socket without error. It does not mean the paper was physically printed.

## 5. Payload Ownership Decision

Laravel owns business payload generation.

For MVP:

- Laravel decides routing.
- Laravel renders receipt layout.
- Laravel creates the ESC/POS byte payload.
- Laravel stores the payload in `print_jobs.content` as base64.
- The agent decodes the base64 payload and sends bytes.
- The agent does not render business templates.
- The agent does not decide receipt content.
- The agent may generate a local diagnostic test receipt only for installation/testing tools.

Reason: receipt formatting, localization, order content, and category routing are business concerns. Keeping them in Laravel avoids version drift across deployed agents and makes the agent a transport/runtime component.

## 6. Printer Ownership Decision

Printers are owned by the approved agent.

Rules:

- Every configured physical printer belongs to exactly one `printer_agents.id`.
- Printer identity inside a site is `agent_id + local_printer_id`.
- Laravel assigns every job to one cloud printer record.
- The printer record determines the owning agent.
- An agent can only claim jobs for printers that belong to that agent.
- If a printer moves to another machine/agent, Laravel must reassign ownership explicitly.

This prevents two agents from racing to print the same printer queue.

## 7. Runtime Model

The agent runs as a Windows service:

- Starts automatically on boot.
- Reads local JSON config.
- Uses an agent token issued by Laravel.
- Sends heartbeat at fixed interval.
- Polls claim endpoint at fixed interval.
- Sends print data through adapters.
- Reports status updates.
- Writes local logs.

The service must continue operating without a visible desktop session.

## 8. Connectivity Model

Allowed:

- Agent to Laravel over HTTPS outbound.
- Agent to LAN printer over TCP from the same site network.

Not allowed:

- Laravel to agent inbound connection.
- Laravel to printer direct connection.
- Browser to LAN printer.
- Public exposure of printer ports.

Default endpoints:

- Cloud base URL: `https://{tenant-domain}`
- Heartbeat: `POST /api/printer-agents/heartbeat`
- Claim jobs: `POST /api/printer-agents/jobs/claim`
- Report status: `POST /api/printer-agents/jobs/{job}/status`

## 9. Agent Authentication

The MVP uses a long random bearer token per approved agent.

Request header:

```http
Authorization: Bearer {agent_token}
```

Required behavior:

- Laravel stores only a hashed token.
- Token is shown once during provisioning.
- Token can be rotated from admin UI.
- Rejected or disabled agents cannot heartbeat, claim, or report status.
- Every request is scoped to the authenticated agent.

Future hardening may add mTLS, but bearer token authentication is the MVP decision.

## 10. Cloud Data Model

### printer_agents

Purpose: installed agent identity and approval state.

Key fields:

- `id`
- `uuid`
- `name`
- `site_id`
- `status`: `pending`, `approved`, `disabled`
- `token_hash`
- `last_seen_at`
- `version`
- `hostname`
- `os`
- `ip_hint`
- `capabilities`
- `created_at`
- `updated_at`

### printers

Purpose: cloud inventory of printers owned by agents.

Key fields:

- `id`
- `agent_id`
- `site_id`
- `name`
- `local_printer_id`
- `connection_type`: `network_raw`, `windows_spooler`
- `ip_address`
- `port`
- `spooler_name`
- `driver_name`
- `status`: `active`, `disabled`
- `capabilities`
- `created_at`
- `updated_at`

Constraints:

- Unique `agent_id + local_printer_id`.
- `ip_address` and `port` required for `network_raw`.
- `spooler_name` required for `windows_spooler`.

### print_jobs

Purpose: durable cloud print queue.

Key fields:

- `id`
- `site_id`
- `printer_id`
- `agent_id`
- `order_id`
- `queue`
- `payload_format`: `escpos_base64`
- `content`
- `status`: `pending`, `leased`, `sent`, `confirmed`, `failed`, `cancelled`
- `available_at`
- `leased_by_agent_id`
- `lease_expires_at`
- `attempts`
- `max_attempts`
- `sent_at`
- `confirmed_at`
- `failed_at`
- `last_error_code`
- `last_error_message`
- `created_at`
- `updated_at`

### print_logs

Purpose: immutable audit trail for job transitions.

Key fields:

- `id`
- `print_job_id`
- `agent_id`
- `printer_id`
- `event`
- `status_from`
- `status_to`
- `message`
- `meta`
- `created_at`

## 11. Claim/Lease Semantics

The claim endpoint leases jobs to one agent for a short time window.

Rules:

- Agent requests jobs for itself.
- Laravel selects jobs where:
  - `agent_id` equals authenticated agent.
  - `status` is `pending`.
  - `available_at <= now`.
  - printer is active.
  - agent is approved.
- Laravel atomically changes selected jobs to `leased`.
- Laravel sets `leased_by_agent_id`.
- Laravel sets `lease_expires_at`.
- If a lease expires without `sent`, Laravel may return job to `pending` or allow a later claim to recover it.

MVP lease duration: 60 seconds.

MVP batch size: 5 jobs.

## 12. API Contracts

### Heartbeat

`POST /api/printer-agents/heartbeat`

Request:

```json
{
  "agent_uuid": "9f8b0b72-4f57-48d6-9e32-3e6dfc3fca01",
  "version": "0.1.0",
  "hostname": "POS-KITCHEN-01",
  "os": "windows",
  "capabilities": {
    "adapters": ["network_raw"],
    "supports_confirmed": false
  },
  "printers": [
    {
      "local_printer_id": "kitchen-lan-9100",
      "name": "Kitchen LAN",
      "connection_type": "network_raw",
      "ip_address": "192.168.1.50",
      "port": 9100
    }
  ]
}
```

Response:

```json
{
  "status": "ok",
  "server_time": "2026-06-08T12:00:00Z",
  "agent_status": "approved"
}
```

### Claim Jobs

`POST /api/printer-agents/jobs/claim`

Request:

```json
{
  "agent_uuid": "9f8b0b72-4f57-48d6-9e32-3e6dfc3fca01",
  "batch_size": 5,
  "lease_seconds": 60
}
```

Response:

```json
{
  "jobs": [
    {
      "id": 12345,
      "printer": {
        "id": 77,
        "local_printer_id": "kitchen-lan-9100",
        "connection_type": "network_raw",
        "ip_address": "192.168.1.50",
        "port": 9100
      },
      "payload_format": "escpos_base64",
      "content": "G0A..."
    }
  ]
}
```

### Report Status

`POST /api/printer-agents/jobs/{job}/status`

Request:

```json
{
  "agent_uuid": "9f8b0b72-4f57-48d6-9e32-3e6dfc3fca01",
  "status": "sent",
  "adapter": "network_raw",
  "sent_at": "2026-06-08T12:00:02Z",
  "duration_ms": 138,
  "error_code": null,
  "error_message": null,
  "meta": {
    "bytes_written": 384,
    "confirmation": "socket_write_only"
  }
}
```

Allowed status reports:

- `sent`
- `confirmed`
- `failed`

Laravel must reject status reports for jobs not leased to the authenticated agent.

## 13. Status Semantics

### pending

Job exists and is available for claim.

### leased

Job is temporarily assigned to one agent for execution.

### sent

Agent successfully wrote bytes to the selected adapter.

For `network_raw`, `sent` means:

- TCP connection opened.
- Payload bytes were written.
- No socket error occurred before close.

It does not prove paper output.

### confirmed

Adapter has evidence stronger than socket write.

For MVP `network_raw`, `confirmed` is not expected unless the specific printer supports a reliable status protocol and the adapter implements it.

### failed

Agent could not send the job.

Common failure codes:

- `printer_unreachable`
- `tcp_connect_timeout`
- `tcp_write_failed`
- `payload_decode_failed`
- `unsupported_payload_format`
- `unsupported_adapter`
- `printer_disabled`

## 14. network_raw Adapter

This is the first adapter to implement.

Inputs:

- Printer IP address.
- Printer TCP port, default `9100`.
- Base64 ESC/POS payload.

Behavior:

1. Decode payload.
2. Open TCP connection with timeout.
3. Write all bytes.
4. Flush/close socket.
5. Return `sent` on success.
6. Return `failed` with concrete error code on failure.

Defaults:

- Connect timeout: 5 seconds.
- Write timeout: 10 seconds.
- Port: 9100.
- Confirmation support: false.

## 15. windows_spooler Adapter

This adapter is Post-MVP.

Purpose:

- Print through installed Windows printer queues.
- Support USB printers and driver-managed printers.

MVP does not depend on it.

Rules for later implementation:

- Keep adapter interface identical to `network_raw`.
- Use printer `spooler_name`.
- Report `sent` when job is accepted by Windows spooler.
- Report `confirmed` only if the spooler/device can prove completion.

## 16. Local Agent Config

Path:

```text
C:\ProgramData\AOTPrinterAgent\config.json
```

Example:

```json
{
  "agent_uuid": "9f8b0b72-4f57-48d6-9e32-3e6dfc3fca01",
  "agent_name": "Kitchen Agent",
  "cloud": {
    "base_url": "https://example.com",
    "token": "shown-once-agent-token",
    "timeout_seconds": 15
  },
  "runtime": {
    "heartbeat_interval_seconds": 30,
    "claim_interval_seconds": 5,
    "batch_size": 5,
    "lease_seconds": 60
  },
  "printers": [
    {
      "local_printer_id": "kitchen-lan-9100",
      "name": "Kitchen LAN",
      "connection_type": "network_raw",
      "ip_address": "192.168.1.50",
      "port": 9100
    }
  ],
  "logging": {
    "file": "C:\\ProgramData\\AOTPrinterAgent\\logs\\agent.log"
  }
}
```

## 17. Security Requirements

MVP:

- HTTPS only.
- Bearer token authentication.
- Token stored in local config with Windows filesystem permissions restricted to Administrators and LocalSystem.
- Laravel stores token hash only.
- Agent authorization scoped by `agent_id`.
- Agent cannot claim another agent's jobs.
- Agent cannot update another agent's printers.
- Status endpoint validates job lease ownership.
- Payload size limit enforced by Laravel.
- Log files must not contain full bearer tokens.

Post-MVP:

- Token rotation workflow.
- mTLS option.
- Signed update channel.
- Local Windows Event Log integration.
- Per-site IP allow list if operationally useful.

## 18. Implementation-Ready Go Project Layout

```text
printer-agent/
  go.mod
  README.md
  docs/
    WINDOWS_INSTALLATION.md
  configs/
    config.example.json
  cmd/
    agent/
      main.go
  internal/
    cloud/
      client.go
    config/
      config.go
    heartbeat/
      heartbeat.go
    jobs/
      claimer.go
      executor.go
      status_reporter.go
      types.go
    logging/
      logger.go
    printers/
      adapter.go
      networkraw/
        adapter.go
    service/
      service.go
  scripts/
    install-service.ps1
    uninstall-service.ps1
```

## 19. Windows Installation Path

The customer-machine installation guide is:

```text
printer-agent/docs/WINDOWS_INSTALLATION.md
```

Deployment model:

1. Build `aot-printer-agent.exe` on the developer machine or CI.
2. Copy the compiled `.exe`, `scripts/install-service.ps1`, and config template to the customer Windows machine.
3. Create `C:\ProgramData\AOTPrinterAgent\config.json`.
4. Run the agent once with `-once` to validate config, heartbeat, and claim connectivity.
5. Install the Windows service from elevated PowerShell.
6. Confirm logs and Laravel heartbeat.

The customer machine does not need Go installed when receiving the compiled executable.

## 20. First Build Plan

### Milestone 1: Service Boots

Goal:

- Build executable.
- Load config.
- Start process as console during development.
- Install as Windows service using script.
- Write startup log line.

Acceptance:

- `agent.exe -config C:\ProgramData\AOTPrinterAgent\config.json` starts without panic.
- Log file is created.
- Service can be installed and started.

### Milestone 2: Heartbeat Works

Goal:

- Agent posts heartbeat to Laravel.
- Laravel updates `last_seen_at`.
- Laravel returns approval state.

Acceptance:

- Approved agent receives `status: ok`.
- Disabled agent receives authorization failure or disabled response.
- Heartbeat includes configured `network_raw` printers.

### Milestone 3: Claim Loop Works

Goal:

- Agent polls claim endpoint.
- Laravel atomically leases pending jobs for the authenticated agent.

Acceptance:

- Agent receives only jobs assigned to its own printers.
- Claimed job changes from `pending` to `leased`.
- Lease expiry is set.

### Milestone 4: Send Test ESC/POS to LAN Printer

Goal:

- Agent sends decoded ESC/POS bytes to configured LAN printer.

Acceptance:

- TCP connection opens to printer IP/port.
- Bytes are written.
- Printer outputs the test receipt.
- Agent logs byte count and duration.

### Milestone 5: Report Sent Status Back to Laravel

Goal:

- Agent reports `sent` after successful socket write.
- Laravel persists final status and audit log.

Acceptance:

- Job changes from `leased` to `sent`.
- `sent_at` is set.
- `print_logs` contains status transition.
- `confirmed` is not emitted unless adapter has reliable confirmation support.

## 21. First E2E Test Scenario

Scenario: one approved agent prints one LAN raw job.

Preconditions:

- Laravel has one site.
- Laravel has one approved agent.
- Agent config contains the matching `agent_uuid` and token.
- Laravel has one active printer:
  - `connection_type`: `network_raw`
  - `ip_address`: LAN printer IP
  - `port`: `9100`
  - owned by the approved agent
- Laravel has one pending print job:
  - assigned to that printer
  - assigned to that agent
  - `payload_format`: `escpos_base64`
  - `content`: valid ESC/POS bytes encoded as base64

Steps:

1. Start the agent.
2. Agent sends heartbeat.
3. Laravel updates agent `last_seen_at`.
4. Agent calls claim endpoint.
5. Laravel leases the pending job.
6. Agent decodes payload.
7. Agent sends bytes to LAN printer over raw TCP.
8. Agent reports status `sent`.
9. Laravel updates job status to `sent`.
10. Laravel creates print log entry.

Expected result:

- Physical receipt prints.
- Job status is `sent`.
- `sent_at` is populated.
- No `confirmed` status is produced for MVP `network_raw`.

Optional confirmed path:

- Only run when the target printer supports reliable status confirmation and the adapter implementation supports that printer protocol.

## 22. Implementation Sequence

1. Create Laravel tables and models.
2. Add agent authentication middleware.
3. Add heartbeat endpoint.
4. Add admin UI to create/approve agents and configure `network_raw` printers.
5. Add print job creation service that produces `escpos_base64`.
6. Add claim endpoint with DB transaction and lease lock.
7. Add status endpoint with lease ownership validation.
8. Build Go agent config loader and logger.
9. Build cloud client.
10. Build heartbeat loop.
11. Build claim loop.
12. Build `network_raw` adapter.
13. Build status reporter.
14. Run first E2E scenario.

## 23. Final Decisions

- Official agent language: Go.
- Official runtime: Windows Service.
- Cloud connectivity: outbound HTTPS only.
- First adapter: `network_raw`.
- First printer target: LAN raw TCP printer on port `9100`.
- Later adapter: `windows_spooler`.
- Payload owner: Laravel.
- Payload format for MVP: `escpos_base64`.
- Printer owner: approved agent.
- MVP success status: `sent`.
- `confirmed`: optional, adapter-specific, not required for first milestone.
