Configuration
Schema for edge-manager.yaml and edge-sync.yaml
The edge client uses two YAML config files. This page is the canonical reference for every supported field.
| File | Loaded by | Purpose |
|---|---|---|
edge-manager.yaml | alloy-edge manager | Identity + cloud connection + (optionally) local process supervision |
edge-sync.yaml | alloy-edge sync | Folder watcher + upload behaviour |
In Docker setups, edge-manager.yaml is generated for you (embedded in the downloaded docker-compose.yml) and edge-sync.yaml is delivered from the cloud — local_state is absent. In binary track-folder setups, you write both files yourself.
Durations use Go-style strings (5s, 1m, 72h). Integer shorthands like 30 are not accepted — quote them as "30s".
Looking for a starter template? The Track Folder setup → walks through a minimal working edge-manager.yaml + edge-sync.yaml pair you can copy and adapt.
edge-manager.yaml
All fields are optional at parse time — the binary will load an empty config. In practice you'll always set backend_url (via the Setup page) and seed_state.api_key.
Top-level
| Field | Type | Default | Description |
|---|---|---|---|
backend_url | string (URL) | built-in default | Alloy backend endpoint. Always set this via the Setup page. Also overridable with ALLOY_BACKEND_URL. |
state_dir | string (path) | /etc/alloy/state | Where the manager persists provisioned credentials and runtime state. Docker setups remap this to /ros2_ws/config/state. Also overridable with ALLOY_STATE_DIR. |
seed_state | object | — | Initial identity + transport. Used on first boot; server-pushed state takes over afterwards. See below. |
local_state | object | — | Local-owned supervision block. When set, its processes and tags are owned by the local config — the server cannot overwrite them. Required for binary track-folder. See below. |
seed_state
| Field | Type | Default | Description |
|---|---|---|---|
api_key | string | — | Provisioning key for first-boot registration. Required to register, but can alternatively be pre-written to {state_dir}/api_key or supplied via ALLOY_API_KEY. |
edge_id | string | /etc/machine-id, then random UUID | Human-readable device identifier reported to Alloy. Also overridable with ALLOY_EDGE_ID. |
tags | map[string, string] | {} | Free-form labels applied on registration. Used for fleet filtering once tag-based features ship. |
transport.http.poll_secs | float (seconds) | 15.0 | How often the manager checks in with the backend for desired-state updates. Sub-second supported. Non-positive values clamp to the default. |
processes[] | list | [] | Fallback processes until the first successful server sync. Same shape as local_state.processes[]. Prefer local_state.processes when you want processes the server cannot override. |
local_state
| Field | Type | Default | Description |
|---|---|---|---|
tags | map[string, string] | {} | Tags the server cannot overwrite. Merged on top of seed_state.tags. |
processes[] | list | [] | Processes supervised locally. Shape below. |
Each entry in processes[]:
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
name | string | — | yes | Identifier for logs and reported_state. |
command | string | — | yes | Shell command to exec. Runs as the user invoking alloy-edge manager. |
enabled | bool | true | no | Kill switch. Set false to define a process but not start it. |
restart | enum | on_failure | no | always / on_failure / never. |
shell | bool | false | no | Wrap command in /bin/sh -c for pipes, redirects, $(...) interpolation. |
trigger | enum | — | no | When to start: boot, cron(<expr>), or connectivity. Without a trigger, processes start when apply() runs. |
duration | duration | — | no | Stop the process after this duration elapses (timed window). |
requires_auth | bool | false | no | Hold the process stopped until the backend approves the device. Set true for anything that authenticates to Alloy with the device API key (alloy-edge sync is the canonical example). Diagnostics, local recorders, and anything that doesn't talk to the data plane can leave this off. |
files | map[string, string] | — | no | Attached config files — filename → inline UTF-8 content. |
edge-sync.yaml
Only input_dir is strictly required. Disk-management fields default to no limit — eviction is FIFO by modification time, oldest first. Files currently uploading or open by another process are never deleted.
Schema version (v0.8+). New configs should start with version: 1. It opts into the current schema — pipeline-based redaction and the cleanup: blocks below — and stops the loader from rewriting the file on every boot. A file with no version: is read as v0 and auto-migrated in memory; run alloy-edge migrate <file> to rewrite it to v1 on disk (a single .bak backup is kept). On read-only mounts (immutable images, ConfigMap volumes) the on-disk rewrite is skipped and the v0 compat shim runs instead — no error.
| Field | Type | Default | Description |
|---|---|---|---|
version | int | — (v0) | Config schema version. Set version: 1 for the current schema (v0.8+). Absent → v0 compat mode + in-memory migration. |
input_dir | string (path) | — | Required. Folder watched for new files. |
file_pattern | string (glob list) | *.mcap,*.json,*.jsonl | Comma-separated globs. A file is scanned if it matches any pattern. |
cycle_time | duration | 1s | How often the watcher scans input_dir. |
upload_delay | duration | 30s | Wait this long after a file's last modification before uploading. Prevents grabbing files still being written. |
state_dir | string (path) | /etc/alloy/state | Where the sync process persists upload bookkeeping. Also accepted under the legacy name status_dir. |
credentials_dir | string (path) | /etc/alloy/state | Where the sync process reads device credentials written by the manager. |
scan_exclude | list of strings | [] | Additional subdirectory basenames to skip during scan. Dot-prefixed dirs (e.g. .alloy-originals) are always skipped on top of this list. |
max_concurrent_uploads | int | 1 | Cap on parallel uploads in flight. |
mcap_require_footer | bool | false | When true, .mcap files without a written footer are never eligible via the age-based fallback — they stay Waiting indefinitely until the footer appears. Default false falls through to the upload_delay age check. |
signed_url_endpoint | string | /signed-url | Backend path for signed-URL requests. The compiled-in default targets a legacy endpoint — set /lake-signed-url explicitly for Alloy's current R2 / data-lake backend. |
metadata | map[string, string] | {} | Key-value metadata sent as x-goog-meta-* headers on GCS resumable uploads. Ignored for S3/R2 presigned PUT uploads. |
txlog_path | string (path) | {state_dir}/txlog.ndjson | Path to the NDJSON transaction log that records every upload attempt. |
max_folder_size | size | — | Cap on total folder size. Format: 10GB, 500MB. |
max_file_age | duration | — | Delete files older than this. Format: 72h. |
max_file_count | int | — | Cap on the number of files retained on disk. |
bwlimit | rate | — | Outbound bandwidth cap. Format: 10M, 1G, 500K (also accepts 10MB/s-style values). |
upload_type | enum | signed_url | signed_url (default, via Alloy backend), none (redact-only — run the filter, write the redacted artefact next to the original, never touch the network; requires redaction.enabled: true), or opendal (upload directly to your own cloud — see below). |
keep_files | bool | — | Deprecated. Use lifecycle instead. Kept for back-compat readouts. |
v0.8 default changes. Two upload/storage defaults flipped in v0.8 — both are reversible:
- Multipart uploads default on.
upload_settings.multipartnow defaults totrue, so files >5 GiB upload via the multipart protocol (/upload/*) instead of a single signed PUT. Setupload_settings.multipart: falseto force the legacy single-PUT path. (Multipart does not yet carry custom uploadmetadata:— keep the legacy path if you rely on that.) - SQLite index store default. The upload bookkeeping store (
index_store.backend) defaults tosqlite(wasjsonl). On first v0.8 boot an existingtxlog.ndjsonis migrated intoindex_store.sqliteand archived totxlog.ndjson.migrated. Setindex_store.backend: jsonlto keep the append-only NDJSON store. include_in_storage_cleanupdefaulttrue→false. Moved-aside files (inlifecycle.<stage>.move_to) no longer count against themax_folder_size/max_file_age/max_file_countbudget by default — seelifecyclebelow. Set<stage>.cleanup.include_in_storage_cleanup: trueto restore the v0.7 behaviour.
lifecycle — what to do with files at each stage
Optional. Replaces the legacy keep_files boolean with per-stage after: actions. Applies whether or not redaction is enabled.
Stage rename (v0.8). The redacted-artefact stage is now lifecycle.transform; the v0.7 name lifecycle.redacted is still accepted as an alias.
lifecycle:
original:
after: keep # keep | delete | move
transform: # v0.7: `redacted:`
after: move
move_to: /var/lib/alloy/redacted| Field | Type | Default | Description |
|---|---|---|---|
original.after | enum | keep | What to do with the unredacted input after a successful upload. keep (fail-safe; pair with max_folder_size to bound disk) / delete / move. |
original.move_to | string (path) | .alloy-originals/ | Destination when original.after: move. Defaults to a sibling dot-dir so the scanner skips it on the next cycle. Relative paths resolve against input_dir. |
transform.after | enum | keep | What to do with the redacted artefact after upload. Same values as original.after. (v0.7 name: redacted.after.) |
transform.move_to | string (path) | .alloy-redacted/ | Destination when transform.after: move. Relative paths resolve against input_dir. (v0.7 name: redacted.move_to.) |
<stage>.cleanup.include_in_storage_cleanup | bool | false | Whether files in this stage's move_to count against the max_folder_size / max_file_age / max_file_count budget. Default flipped to false in v0.8 (was true in v0.7) — moved-aside files stay out of the cleanup budget unless you opt in. |
lifecycle.original.move_to and lifecycle.transform.move_to must differ when both stages use after: move — v0.8 rejects a shared destination at load time, since the original and its redacted artefact would otherwise overwrite each other.
Redaction pipeline — strip or hash fields before upload
Optional. Runs a redaction rules file over each recording before upload — the rules themselves (channel filter, per-topic transforms, metadata mappings) live in that file; this section is the edge-sync.yaml wiring that points at it and sets the I/O knobs. Use lifecycle to control retention/movement of original and redacted files. See Redact for the why-and-when.
In v0.8+ redaction is one step of a pipeline: tap-chain. The v0.7 flat redaction: block is still accepted — it auto-migrates to an equivalent single-step pipeline at runtime (run alloy-edge migrate to rewrite it on disk).
pipeline: is an ordered tap-chain. A file flows top→bottom through the steps; a step whose transform: is set produces a redacted artefact from the rules file. The transform behaviour knobs (failure policy, output compression, audit) live under lifecycle.transform, not on the step.
version: 1
pipeline_trigger: delay-after-close # delay-after-close (default) | close
pipeline:
- transform: .alloy/redaction.yaml # rules file → redacted artefact
transform_suffix: redacted # artefact infix; <stem>.redacted.mcap
upload: true # upload the artefact
original_after: delete # keep (tap) | delete | move — flow control for the source
transform_after: keep # keep | delete | move — disposition of the artefact
# file_pattern: "**/camera/*.mcap" # restrict step to matching files; absent → all
# filter: # gate the step on recording content
# require_topics: ["/nav/**"]
# min_duration: 60s
# min_messages: 100
# min_size: 10MB
# mcap_require_footer: true
lifecycle:
transform:
on_rule_error: skip_record # skip_record | skip_file | pass_original
on_reader_error: skip_tail # abort | skip_tail | recover
output_compression: inherit # inherit | none | zstd | lz4
audit:
jsonl_path: .alloy/state/redaction-audit.jsonl
embed_in_mcap: trueEach pipeline[] step:
| Field | Type | Default | Description |
|---|---|---|---|
transform | string (path) | — | Path to the redaction.yaml rules file. A step with no transform: is route-only (move/drop without rewriting). |
transform_suffix | string | step index | Infix inserted between the input stem and extension for the artefact (mission.mcap → mission.redacted.mcap). |
upload | bool | false | Upload this step's artefact. Requires transform:. |
original_after | enum | lifecycle.original.after | Flow control for the source at this step: keep (tap — keep it flowing to later steps), delete, or move. |
transform_after | enum | lifecycle.transform.after | Disposition of this step's artefact: keep / delete / move. |
file_pattern | string (glob) | — | Restrict the step to matching files (** crosses /). Absent → all files. |
filter | object | — | Gate the step on recording content. Keys: mcap_require_footer (bool), require_topics (glob list), min_duration (duration), min_messages (int), min_size (size). A missing footer waits and re-checks next cycle; a content-predicate miss (topics / duration / messages / size) skips the step. |
pipeline_trigger (top-level): delay-after-close (default — wait upload_delay after the writer closes the file) or close (act as soon as the file is closed).
The behaviour knobs (on_rule_error, on_reader_error, output_compression, audit) are documented in the legacy tab and apply identically — in the pipeline form they sit under lifecycle.transform.
The flat redaction: block. Still accepted in v0.8 — the loader desugars it to an equivalent single-step pipeline at runtime. Prefer the pipeline form for new configs.
redaction:
enabled: true
rules_file: .alloy/redaction.yaml
on_rule_error: skip_record # skip_record | skip_file | pass_original
on_reader_error: skip_tail # abort | skip_tail | recover
output_compression: inherit # inherit | none | zstd | lz4
output_suffix: redacted # filename infix; <stem>.redacted.mcap
audit:
jsonl_path: .alloy/state/redaction-audit.jsonl
embed_in_mcap: true| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Master switch at the edge-sync level. Even with redaction.yaml itself enabled, the redactor is bypassed unless this is true. |
rules_file | string (path) | — | Required when enabled: true. Path to the redaction.yaml rules file. |
on_rule_error | enum | skip_record | What to do when a transform rule blows up on a single record. skip_record — drop the bad record and keep filtering (default). skip_file — abort the file; the original stays put and is reported as FilterFailed. pass_original — fail-open: the unredacted file is uploaded. Use pass_original only with explicit operator opt-in. |
on_reader_error | enum | skip_tail | What to do when the MCAP reader hits malformed bytes (truncated chunk, missing footer, summary CRC mismatch). Distinct from on_rule_error. abort — fail the file. skip_tail — emit everything up to the bad chunk and stop. recover — best-effort scan past corruption. |
dry_run | bool | — | Deprecated. Use upload_type: none plus lifecycle.{original,redacted}.after: move to .dry-run/ instead. Still parses with a deprecation warning; the loader expands it to the equivalent overrides. See Dry-run before flipping it on. |
output_compression | enum | inherit | Chunk compression for the redacted MCAP. inherit mirrors the input's first-chunk compression. none / zstd / lz4 pin a specific codec. |
output_suffix | string | redacted | Infix inserted between the input's stem and extension for the redacted artefact (e.g. mission.mcap → mission.redacted.mcap). Local filename and cloud object key stay in lockstep with this value. |
output_dir | string (path) | — | Where redacted files go. Unset (default): sibling-file behaviour — <input_dir>/.alloy-redacted/<stem>.<output_suffix>.<ext>. Set: the relative path from input_dir to the original is mirrored under output_dir and the suffix is still applied. Relative paths resolve against input_dir. |
quarantine.mode | enum | delete | Deprecated. Use lifecycle.original.after (delete / move) instead. |
quarantine.dir | string (path) | {state_dir}/quarantine | Deprecated. Use lifecycle.original.move_to instead. |
audit.jsonl_path | string (path) | — | Set to write a JSONL sidecar — one line per redacted file. Unset disables the JSONL sidecar. Independent of embed_in_mcap. |
audit.embed_in_mcap | bool | true | Embed the audit summary as an MCAP metadata record named alloy.redaction.audit inside the redacted file, so the file documents itself. Set false only when the rule layout is itself sensitive. |
Upload directly to your own cloud (OpenDAL)
Set upload_type: opendal and add an opendal: block. Requires an alloy-edge build with the opendal feature.
| Field | Used by | Description |
|---|---|---|
scheme | all | s3, gcs, azblob, or fs |
root | all | Prefix path (e.g. /fleet/robot-01/recordings) |
bucket | s3, gcs | Bucket name |
region | s3 | AWS region |
endpoint | s3 | Custom endpoint (MinIO, R2) |
container | azblob | Azure container name |
account_name | azblob | Azure storage account |
credential_path | gcs | Service-account JSON path |
Options are passed through to OpenDAL. See the OpenDAL service docs for the complete per-scheme options each scheme supports.