# Architecture > Authoritative source: [AetherCloud](https://github.com/EvanL1/AetherCloud/blob/main/docs/concepts/architecture.md). This page is mirrored into the unified AetherIoT documentation. AetherCloud starts as a modular monolith: one repository, one set of domain and application modules, and several small composition roots that can run and scale independently. This keeps transactions and refactors local without coupling long-lived edge sessions to request/response API workloads. ## Target process model ```text application modules / | \ HTTP API CloudLink workers | | | REST / MCP CloudLink/MQTT IaC/jobs/projections web console | HTTP API ``` - `api` serves REST, OpenAPI, WebSocket notifications, and later MCP Streamable HTTP. - `cloud-link` owns authenticated, long-lived AetherEdge sessions and protocol acknowledgement. - `workers` execute isolated infrastructure plans, refresh provider inventory, advance deployments and telemetry projections, retry work, and deliver the outbox. - `web` is an authenticated client of the API and has no privileged data path. Only the API is a configured long-running process in the repository-foundation milestone. It now exposes authenticated audit JSON and finite resumable SSE snapshots in addition to public liveness/product metadata; its configured bearer identity and in-memory audit adapter are not production IAM or durability. `apps/mcp` is an implemented transport-neutral resource/tool interface, not a runnable MCP composition root. It delegates Audit, Data Export, and governed Job operations to application use cases; MCP SDK transport and identity composition remain planned. The CloudLink package now exposes an independently startable experimental MQTT ingress lifecycle backed by strict codecs and application use cases, but it has no production environment composition or durable PostgreSQL adapters. Workers remain planned. The experimental ingress must not be presented as a production CloudLink service. ## Dependency direction ```text domain <- ports <- application <- interfaces/composition roots ^ +---- adapters ``` Domain code owns identifiers, values, invariants, and state transitions. Ports describe capabilities such as an artifact repository or job ledger. Application code authorizes and coordinates use cases. Adapters implement ports using PostgreSQL, object storage, identity providers, and network transports. Provider adapters also implement application ports. They expose declared capabilities and provider-native extensions without leaking vendor SDK types into domain or application modules. OpenTofu is the default infrastructure engine and Terraform is compatible through the same engine port. Concrete infrastructure never appears in domain names. Prefer `JobLedger` to a generic database port and `ArtifactStore` to an object-storage SDK wrapper. ## Initial bounded contexts - Identity and access - Provider catalog and cloud connections - Cloud inventory and normalized resource graph - Placement and deployment stacks - Fleet and topology - Telemetry and alarms - Artifacts and releases - Deployments and desired state - Capability jobs - Audit - Integrations and export Operational observability is a cross-cutting adapter concern rather than a business bounded context. OpenTelemetry SDK types do not enter the domain or application packages, and its signals do not replace IoT telemetry or audit. Shared code is intentionally small. A bounded context exports a contract rather than exposing its internal records to another context. ## Data and messaging PostgreSQL is the default transactional AetherCloud product store. The first Gateway Identity SQL adapter and migration are implemented, while composition root wiring and every other PostgreSQL bounded-context adapter remain planned. Infrastructure state is different: each provider-scoped deployment stack uses its own remote, locked backend. Workers are stateless with respect to infrastructure state and consume saved JSON plans rather than scraping terminal output or raw state text. The first background-work implementation should use a transactional outbox and PostgreSQL-backed workers. A broker is introduced only when measured throughput, retention, or consumer isolation requires it. The first experimental CloudLink wire contract uses versioned strict JSON over MQTT so TypeScript and Rust can execute the same fixtures. A later binary encoding requires joint AetherCloud/AetherEdge review and cannot change business identity or acknowledgement semantics. Sequence fields remain canonical decimal strings without unsafe JavaScript-number conversion. See [multi-cloud fusion](/en/aethercloud/concepts/multi-cloud-fusion) for provider, state, credential, and execution boundaries. ## Service extraction rule A module becomes a separately owned service only when at least one of these is measured: - materially different scaling characteristics - a security or failure-isolation boundary - an independent deployment cadence owned by another team - a persistence or regional-placement requirement that cannot be met in-process The extraction must preserve the application contract and add failure-mode tests before changing deployment topology. ## IoT context collaboration The complete bounded-context map is maintained in the [IoT Cloud capability map](/en/aethercloud/concepts/iot-cloud-capability-map). Contexts expose application contracts or events rather than database records: ```text Identity and Access -> authorization decisions Fleet Identity -> CloudLink credential verification Runtime Catalog -> deployment compatibility and Job eligibility Artifact Registry -> immutable Deployment revision references CloudLink -> application ingestion use cases Application transactions -> audit + outbox Outbox -> projections, notifications, webhooks, exports, SSE, and WebSocket ``` HTTP, CLI, MCP, and CloudLink interfaces all decode external input and call the same command/query application API. An edge observation that changes a cloud projection is an authenticated ingestion command even when the edge-originated fact is descriptive. Query does not mean "safe to write because the data came from an edge." ## HTTP and CloudLink boundary HTTP authenticates people and service accounts, evaluates Tenant authorization, and creates cloud intent such as desired state or a governed Job. It persists that intent and an outbox record; it never sends through a live edge socket. CloudLink authenticates Gateway credentials and owns session fencing, protocol negotiation, stream sequence, durable acknowledgement, resume, and backpressure. It reads application-owned mailboxes and invokes the owning bounded context for telemetry, manifest, deployment, or Receipt ingestion. It does not implement those business state machines or expose arbitrary RPC. This is why the API and CloudLink composition roots are independent even while the product remains a modular monolith. Request/response traffic and long-lived connections have different scaling, draining, and failure behavior. Workers are a third independent root for outbox delivery, projections, retries, retention, and governed background execution. ## Authority and data placement | Store | Responsibility | Explicitly not authoritative for | | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | | PostgreSQL | Tenant/IAM, Fleet identity, latest manifests, session cursors, artifact metadata, desired/reported/applied metadata, Job ledger, Receipt index, audit, inbox, outbox, quota | edge live point state; provider resources; infrastructure State | | Object storage | immutable artifacts, signatures, provenance, raw/cold telemetry batches, large evidence, exports | mutable aggregate state or credentials | | Time-series or analytics storage | historical telemetry/event projection, downsampling, aggregates | current edge point value or alarm truth | | Deployment-stack remote backend | one provider-scoped locked infrastructure State | IoT Fleet, telemetry, or cross-provider global state | PostgreSQL may initially implement a history-store adapter, but the application port remains independent so measured ingestion or analytical requirements can select a different adapter later. Operational traces and metrics leave composition roots through an optional OpenTelemetry adapter and OTLP exporter. Their backend has no transactional or authority role. Exporter failure cannot change a command, durable acknowledgement, or worker result. Desired, reported, and applied are separately persisted facts. A projection always carries source observation time and freshness. No database choice changes the authority defined by the edge/cloud/provider boundary. ## Transactional inbox and outbox A command transaction atomically writes its aggregate change, required audit record, and outbox message. Edge ingestion atomically records its de-duplication identity and accepted business fact before CloudLink acknowledges it. Workers lease work, retry within a bounded policy, and make dead-letter state visible. PostgreSQL-backed delivery is the default. Kafka or another broker is introduced only after measured throughput, retention, or consumer-isolation requirements, and never changes command idempotency or durable acknowledgement semantics. Outbound webhook attempt intent is persisted before network I/O, uses one stable delivery identity across bounded retries, and becomes a visible dead letter when exhausted. Data exports publish immutable object references rather than returning unbounded history through the API process. See [audit and integrations](/en/aethercloud/concepts/audit-and-integrations). Multi-cloud portability is expressed through independently deployable cells and Provider database profiles, not a generic lowest-common-denominator database API. One cell has one authoritative PostgreSQL writer topology; a Tenant has an explicit home cell. Cross-cloud backup, disaster recovery, and Tenant migration require fencing and governed workflows. Read [PostgreSQL persistence and multi-cloud cells](/en/aethercloud/concepts/persistence-and-multi-cloud-cells). ## Tenant isolation Every Tenant-owned aggregate, event, unique constraint, object prefix, inbox, outbox, cache key, and analytical partition carries `TenantId`. A user or service-account interface resolves Tenant context from authenticated identity; a Gateway interface resolves it from the verified claim or active credential. Body and path identifiers select resources only after that context exists. The planned PostgreSQL adapter combines application-enforced scope, composite Tenant keys, and row-level security as defense in depth. Cross-Tenant access is available only through an explicit platform use case with separate permission and audit evidence. Before implementing a CloudLink integration, read the [CloudLink MQTT reference](/en/aethercloud/reference/cloudlink-mqtt-v1) and [CloudLink reliability and lifecycle](/en/aethercloud/concepts/cloudlink-and-core-state-machines). Read [IoT business telemetry](/en/aethercloud/concepts/iot-telemetry) before adding history or ingestion and [operational observability](/en/aethercloud/concepts/operational-observability) before adding instrumentation. Use the [IoT Cloud roadmap](/en/aethercloud/guides/iot-cloud-roadmap) to distinguish capabilities available now from those that remain planned. --- # Artifact registry and immutable publication > Authoritative source: [AetherCloud](https://github.com/EvanL1/AetherCloud/blob/main/docs/concepts/artifact-registry.md). This page is mirrored into the unified AetherIoT documentation. AetherCloud publishes Pack, configuration, model, rule, and application revisions as immutable, content-addressed facts. Publication makes a revision eligible for a later desired-state reference; it does not deploy, download, or apply anything at an AetherEdge runtime. ## Authority and boundaries - AetherCloud is authoritative for artifact metadata, validation and publication state, signer policy, and release-channel references. - The content store is authoritative for the immutable bytes identified by a SHA-256 digest. - AetherEdge remains authoritative for local compatibility checks, cache state, and whether a revision is accepted and applied. - A release channel is a governed reference. It is not mutable content and is not evidence of deployment. - Signature verification and publication audit are business requirements; sampled OpenTelemetry signals cannot replace either. ## Implemented inner-layer slice The domain package defines bounded IDs, canonical uint64 decimal revision and content-length values, SHA-256 digests, compatibility requirements, signature metadata, and this immutable state machine: ```text draft -> validated -> published -> deprecated -> withdrawn \---------------------> withdrawn ``` Every transition returns a frozen new revision and preserves identity, content digest, compatibility, dependencies, signature, and publication timestamps. Publication cannot skip validation. Withdrawal retains prior publication evidence rather than deleting history. The application package implements `artifact.revision.publish` and `artifact.revision.get`. Publication is a high-risk, explicitly confirmed, expiring, idempotent, audited Tenant/Project command. It decodes external input exactly, verifies content and signature through application-owned ports, and only then asks the repository to atomically persist the published revision, channel reference, idempotency evidence, audit evidence, and outbox evidence. The memory adapter is a conformance implementation. It proves: - exact retry returns the original revision without duplicate audit/outbox evidence; - reuse of an idempotency key with changed publication identity conflicts; - a revision identity cannot be replaced; - a release-channel race cannot silently replace its current revision; - content-length/digest and trusted-signature checks fail closed; - persistence failure writes no revision, channel, audit, or outbox evidence; - Tenant/Project-scoped lookup does not reveal another Tenant's revision. ## Storage contract The production design separates three responsibilities: | Store | Responsibility | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | PostgreSQL | artifact/revision metadata, publication state, channel reference, idempotency record, authorization scope, audit and transactional outbox | | Object storage | immutable content, signature material, provenance, SBOM and large validation evidence by digest | | Edge cache | locally verified content and application evidence under AetherEdge policy | The current implementation has no PostgreSQL or object-storage adapter. The memory adapter's `putContent` and trusted-signature helpers are test fixtures, not upload, KMS, CA, or production signer APIs. ## Failure and replay semantics Content missing, digest/length mismatch, invalid signature, verifier/storage unavailability, revision conflict, release-channel conflict, and idempotency conflict are typed failures. No failure is converted into a successful publication. An HTTP timeout around a future production route can be resolved by replaying the same idempotency key; clients must query rather than invent a publication result. Publication events are currently present only in the atomic memory adapter. `artifact.revision-published.v1` becomes a production integration event only after a durable transaction writes a real outbox. ## Still planned - upload/finalize and immutable object-storage adapters; - PostgreSQL metadata, audit, idempotency, channel, and outbox transactions; - production signer policy, KMS integration, provenance, SBOM and malware validation; - deprecate, withdraw, and channel-move application commands; - HTTP, CloudLink, export, and MCP interfaces; - AetherEdge artifact download and verification counterpart; - desired/reported/applied deployment and rollout. Next, read [Desired, reported, and applied deployment](/en/aethercloud/concepts/desired-reported-applied-deployment) to understand how a published artifact becomes a governed rollout without being mistaken for proof that an edge runtime applied it. --- # Audit, subscriptions, webhook delivery, and data export > Authoritative source: [AetherCloud](https://github.com/EvanL1/AetherCloud/blob/main/docs/concepts/audit-and-integrations.md). This page is mirrored into the unified AetherIoT documentation. This context turns application evidence into queryable audit history and governed outbound work. It does not make OpenTelemetry a business ledger and it does not let an HTTP, SSE, webhook, export, or future MCP interface write a bounded-context store directly. ## Authority and transaction boundary An owning command transaction must commit its aggregate change, required audit event, and outbox record together. Integration workers consume committed outbox facts later. A failed webhook or disconnected SSE client can delay an external projection, but cannot roll back or change the original business outcome. Audit is append-only evidence scoped by `TenantId` and `ProjectId`. Operational traces may correlate through a trace identifier, but sampled OpenTelemetry data is neither authorization evidence nor a replacement for audit. ## Implemented layers The following executable foundations exist: - an immutable `AuditEvent` domain value with canonical lossless sequence, subject, resource, outcome, governance, correlation, and optional evidence digests; - the authorized `SearchAuditEvents` application query and a Tenant/Project scoped memory repository; - authenticated `GET /api/v1/audit/events` and a finite resumable `GET /api/v1/audit/events/stream` SSE snapshot, both calling the same query; - `WebhookSubscription` create, disable, and get use cases using stable destination references and bounded event allowlists; - a `WebhookDelivery` lifecycle with persisted in-flight intent, a stable delivery idempotency key, bounded retries, attempt evidence, visible dead letter state, and explicit-confirmation redrive; - a `DataExport` lifecycle for bounded audit, alarm, or telemetry history exports, with high-risk explicit-confirmation request, worker outcome reporting, immutable object reference, digest, and lossless byte length; - memory conformance adapters that atomically retain aggregate, idempotency, audit, and outbox evidence for application tests. The SSE endpoint is deliberately a finite snapshot. `Last-Event-ID` resumes from the audit sequence through the same application query, but no durable live-notification process exists yet. ## State machines ```text Webhook subscription: active -> disabled Webhook delivery: pending -> delivering -> delivered \-> retrying -> delivering \-> dead-lettered -> pending (explicit redrive) Data export: queued -> running -> ready \-> failed queued/ready/failed -> expired ``` An attempt is written as `delivering` before the external sender is called. A crash can therefore leave an in-flight attempt requiring worker reconciliation; it must not be silently treated as never attempted. The delivery identity is the receiver-facing idempotency key across retries. A conflicting enqueue or redrive fails closed. An export response never contains raw export bytes. `ready` exposes only a bounded object reference, content digest, and decimal `uint64` byte length. A separate authorized download capability and production object-store adapter remain required before clients can retrieve content. ## Destination and SSRF boundary Application and domain records contain `WebhookDestinationId`, never an arbitrary request-supplied URL or plaintext signing secret. A production sender must resolve the reference from a Tenant-scoped secret-backed destination registry, require HTTPS, reject private/link-local/reserved address targets after DNS resolution and redirect, bound response bytes and time, sign the canonical delivery, and prevent credential forwarding. That sender and registry are planned; the current memory sender is test-only. ## Missing production surface PostgreSQL append-only audit, transactional outbox consumption, a destination registry, secret rotation, production HTTP sender, signing, DNS/redirect SSRF defence, retry leasing, a live SSE notifier, WebSocket, object storage, export workers, retention/quota enforcement, and public webhook/export APIs are still planned. The current API uses an in-memory audit repository and a configured bearer identity, so it is an executable interface for local composition and contract tests rather than a production identity or durability boundary. Use the [HTTP API reference](/en/aethercloud/reference/http-api) and the [application contract catalog](/en/aethercloud/reference/application-contracts) to find the supported integration operations, required permissions, and current implementation status. --- # CloudLink reliability and lifecycle > Authoritative source: [AetherCloud](https://github.com/EvanL1/AetherCloud/blob/main/docs/concepts/cloudlink-and-core-state-machines.md). This page is mirrored into the unified AetherIoT documentation. This page mixes executable inner-layer foundations with planned end-to-end behavior. An experimental JSON/MQTT v1 codec, application bridge, independently startable ingress lifecycle, Schemas, fixtures, and real-broker harness now exist, and the public AetherContracts alpha.3 release is the sole shared authority. The opt-in real Mosquitto dual harness and fault matrix are executable alpha evidence. They do not constitute a production process; production authentication and PostgreSQL durability remain gates. Production readiness requires authentication, one wire profile, shared fixtures, a dual real-Broker harness, fault injection, crash-durable persistence, and an explicit legacy cutover in that order. ## CloudLink responsibility CloudLink authenticates a Gateway credential, negotiates a protocol version, fences sessions, transfers versioned envelopes, maintains per-stream sequence and durable acknowledgement cursors, applies backpressure, and resumes after disconnect. It invokes application ingestion and delivery use cases. CloudLink does not own telemetry meaning, deployment policy, Job execution, or alarm lifecycle. It cannot directly write SHM, a point, or a device register; invoke arbitrary runtime methods; activate unpublished artifacts; bypass Job confirmation; or clear an edge-authoritative alarm. HTTP creates desired state and governed Jobs in transactional storage and an outbox. CloudLink reads an application-owned outbound mailbox. An HTTP handler never writes a live edge socket. ## Gateway enrollment Implemented foundation: ```text registered -> awaiting-claim -> claimed | expired ``` Planned production extension: ```text claimed -> credential-pending -> active <-> suspended -> revoked network loss | changes no state v recovery-pending | active (new generation) ``` - `now >= claim.expiresAt` rejects a claim. - An identical claim request and credential-request fingerprint is an idempotent replay. A different request cannot rebind a claimed Gateway. - A claim that succeeds before certificate issuance may resume credential binding without consuming a new token. - Disconnect does not revoke identity. Revocation fences a credential generation; recovery never revives it. - Registration or claim success is not evidence that CloudLink authentication is active. ## CloudLink session The domain/application/memory foundation implements credential verification, server-preference protocol negotiation, monotonic session epochs, old-session fencing, lossless per-stream resume cursors, authenticated heartbeat, and a Tenant-scoped current-session query. The experimental MQTT layer adds strict wire decoding, topic/session binding, non-retained QoS-1 enforcement, an application bridge, and lifecycle composition. PostgreSQL, multi-instance socket ownership, timeout scheduling, credit flow control, durable inbox/outbox, and production configuration remain planned. The implemented structural signature evidence is not sufficient shared-Broker production authentication. Alpha.3 first uses a signed Cloud challenge and a Gateway signature, then binds every generic-Broker uplink to that session with a Gateway signature. A Broker-specific adapter may instead provide verified out-of-band publisher attestation for every publish. Payload-supplied attestation, topic identity, or one successful hello cannot authenticate later messages. Cloud challenges use the proposal signature transcript; alpha.3 durable application ACKs are explicitly unsigned. ```text authenticating -> negotiating -> resuming -> active -> draining -> closed | | | | rejected rejected rejected suspect -> active or closed ``` - Each authenticated connection gets a monotonic session epoch. A newer epoch fences the old connection, including late messages from the old socket. - Resume begins at AetherCloud's last durable acknowledgement for each stream. - Same identity and digest is a duplicate and receives the same acknowledgement. Same identity with a different digest is quarantined as a security conflict. - A sequence gap requests replay and does not advance the cursor. Out-of-order envelopes may be durably buffered within a bounded window. - Heartbeat timeout moves the connection projection to `suspect` or `closed`; it does not prove that the Gateway or downstream devices stopped. - Explicit credit/window flow control and bounded queues prevent a fast sender from exhausting process or Tenant resources. ## Runtime Manifest report The implemented transport-neutral report command accepts the closed AetherEdge Runtime Manifest v1 shape only after credential authentication. It verifies the same canonical-JSON SHA-256 contract as AetherEdge, represents its generation as a canonical unsigned 64-bit decimal string, and derives Tenant, Project, and Gateway scope from the verified credential rather than payload identifiers. ```text no current -> accepted-latest older generation -> accepted-late (history only) same generation + same digest -> replayed same generation + different digest -> rejected conflict newer generation -> accepted-latest ``` Late observations remain queryable history but cannot move the latest capability projection backward. A capability report is compatibility evidence, not authorization to invoke an edge method. The experimental CloudLink MQTT report envelope is implemented. PostgreSQL projection, public query transport, durable audit, and outbox remain planned. ## Artifact publication ```text draft -> validated -> published -> deprecated -> withdrawn \---------------------> withdrawn ``` - Once a content digest identifies a revision, its bytes and compatibility metadata are immutable. Corrections create a new revision. - Publication requires successful validation, provenance, compatibility, and signature policy. Partial validation does not publish. - A channel is a separately audited movable reference; moving it does not edit a revision. - Withdrawal prevents new deployments but keeps blobs, signatures, and applied evidence needed for audit and recovery. - Repeated upload or publish with the same digest and idempotency key returns the original result; conflicting content is rejected. The frozen domain transitions, governed publish/query application use cases, and atomic memory content/signature/metadata/audit/outbox adapter are implemented. Upload/finalize, production object storage and signer policy, PostgreSQL metadata, durable audit/outbox, channel movement, lifecycle HTTP, and edge artifact delivery remain planned. See the [artifact registry](/en/aethercloud/concepts/artifact-registry) for exact layer status. ## Telemetry batch ingestion ```text received -> decoded -> validated -> persisted -> acknowledged -> projected | | | | rejected rejected quarantined pending-gap ``` - Batch identity is Tenant, Project, Gateway, logical stream, stream epoch, and first position. JSON encodes protocol 64-bit positions as canonical decimal strings. - The MVP batch is atomic. One invalid record rejects the batch before any business record becomes accepted. - Same identity and business-content digest returns the prior durable acknowledgement. Same identity with different content is a conflicting duplicate and is quarantined. - A gap may retain a batch in a bounded durable reorder window but cannot advance the contiguous cursor. Overflow becomes an explicit loss marker. - Persistence failure returns no successful acknowledgement. An ambiguous commit is resolved by the same identity on replay. - Projection failure after durable acceptance does not revoke the receipt; workers retry projection from the outbox. - Disconnect changes no batch state and the edge may replay every unacknowledged position. - Batch cancellation is unsupported after ingress. Retention and deletion are separate governed lifecycle operations, not cancellation of historical fact. ## Deployment desired, reported, and applied Rollout state: ```text planned -> running <-> paused -> completed | completed-with-failures v cancelling -> cancelled ``` Target state: ```text pending -> offered -> accepted -> fetching -> validated -> applied | | | | expired rejected failed failed or unknown ``` - A desired generation is monotonic cloud intent. Reported and applied are separate edge observations with their own generation, time, and evidence. - A late observation for an older generation is retained but cannot roll the latest projection backward. - `offered`, `accepted`, `fetching`, and `validated` never imply `applied`. - A network timeout yields `unknown`. A late edge receipt may later resolve it. - Cancellation stops work that has not crossed the relevant edge acceptance boundary. It cannot erase work already performed. - Rollback creates a new desired generation pointing at an older immutable revision; it never edits history. - Partial success is a first-class rollout result. Compensation is a new, auditable rollout. ## Governed capability Job ```text created -> awaiting-confirmation -> authorized -> queued -> offered | | | | expired rejected expired accepted -> running | | | succeeded failed partial | unknown ``` - Permission, risk policy, confirmation, precondition, idempotency, expiry, and audit requirements fail closed. - The edge may reject work because of commissioning, compatibility, authorization, or safety policy. - Timeout after offer or execution is `unknown`, not failure and not permission to create a new non-idempotent physical action. - Retry uses the same Job identity and is allowed only when the capability declares safe replay semantics. - Cancellation is a request. If work has started or completed, its actual Receipt remains authoritative. - A late Receipt can resolve `unknown`; it cannot rewrite earlier audit events. ## Command Receipt ```text ingressed -> authenticated -> persisted -> acknowledged -> projected | | | rejected quarantined pending-predecessor ``` - Receipts are append-only facts with identity, payload digest, Job causation, edge sequence, observation time, and optional evidence digest. - Duplicate identity and digest is replay-safe. Same identity with different content is quarantined and never acknowledged as success. - A Receipt arriving before an expected predecessor is durably retained and marked pending rather than discarded. - Acknowledgement occurs only after Receipt, inbox de-duplication, and evidence reference are atomically durable. - Large evidence is content-addressed in object storage. Missing or unmatched evidence remains visible. - Projection can move an earlier Job from `unknown` to a terminal result while preserving the full timeline. ## Alarm projection Edge occurrence facts: ```text unseen -> active -> updated* -> cleared \-> superseded or retracted correction ``` Cloud workflow overlay: ```text unacknowledged -> acknowledged freshness: complete | gap | stale ``` - Edge alarm identity, generation, and sequence make ingestion replay-safe. - An out-of-order clear is retained as pending and marks the projection incomplete until replay fills the gap. - Disconnect marks freshness stale; it does not clear or resolve an alarm. - Store-and-forward overflow creates a visible gap or data-loss marker. - Cloud acknowledgement, comment, notification mute, and assignment are cloud workflow facts. They never change the edge's active/cleared fact. - A re-raised condition uses a new occurrence or explicit generation rather than silently reopening a cleared record. ## Webhook delivery ```text pending -> leased -> delivered | | disabled retry-wait -> leased | dead-letter -> redrive-requested -> pending ``` - One integration event and endpoint produce a stable delivery identity. Releasing or retrying a lease never creates a new business event. - HTTP timeout is an unknown remote-consumption result, so retry keeps the same event and delivery identity. - Retry uses bounded exponential delay and an attempt ceiling. Exhaustion makes dead-letter state visible instead of silently discarding the event. - Endpoint disablement stops new leases but cannot undo a remote side effect. Deletion retains required audit and delivery history. - Redrive is a governed command with permission, idempotency, expiry, and audit; it creates another attempt, not another event. - Partial fan-out success remains visible per endpoint. No distributed transaction is claimed across subscribers. These rules preserve the edge-first authority boundary: the edge remains authoritative for live state and physical control, while the cloud records accepted facts and governed intent without claiming unproven device outcomes. --- # Desired, Reported, and Applied deployment > Authoritative source: [AetherCloud](https://github.com/EvanL1/AetherCloud/blob/main/docs/concepts/desired-reported-applied-deployment.md). This page is mirrored into the unified AetherIoT documentation. The cloud-side domain/application foundation and atomic memory adapter are implemented for one Gateway target. They provide published-Artifact lookup, lossless desired generations, distinct reported and applied facts, pause/resume/cancel-request/rollback intent, authenticated edge observations, unknown outcome, replay/conflict handling, Tenant-scoped query, audit evidence, and outbox evidence. This is not an end-to-end rollout. PostgreSQL, a production audit/outbox, CloudLink outbound delivery and observation envelopes, target snapshots, canary/batch scheduling, public HTTP, and the AetherEdge counterpart remain planned. ## Authority and facts - AetherCloud owns desired revision and rollout intent. - AetherEdge owns its reported observation and final compatibility/policy decision. - Applied exists only when edge evidence says applied or failed. A dispatch, download, validation, connected session, or HTTP success cannot create it. - A network timeout creates a separate `unknown` reconciliation state. It does not fabricate an Applied fact. A later authenticated edge observation may resolve the projection. - Artifact publication makes a revision eligible for Desired; it does not deploy or apply it. The implemented view returns `reported: null` and `applied: null` until those facts exist, rather than overloading a single status field. ## State and ordering The single-target rollout state foundation is: ```text running <-> paused -> cancel-requested | | +-- edge applied ----> completed +-- reject/fail -----> completed-with-failures ``` Rollback creates a newer Desired generation pointing at another immutable, published Artifact Revision. It appends desired history; it does not edit the prior generation or imply the edge restored anything. Every edge observation has a stable identity and lossless Desired generation. Exact replay is safe. Reuse of an observation identity with different content conflicts. A fact for an older generation is retained in observation history without rolling the current projection backward. A fact newer than the cloud's known Desired generation fails closed. Cancellation is a request to stop work that has not crossed the relevant edge boundary. It cannot erase applied evidence or reverse a physical effect. ## Implemented application surface - `deployment.rollout.start`: high risk, explicit confirmation, published Artifact precondition, expiry, idempotency, permission, and audit required. - `deployment.rollout.pause`, `deployment.rollout.resume`, and `deployment.rollout.cancel-request`: governed rollout intent changes. - `deployment.rollout.rollback`: high-risk new Desired generation with a published Artifact precondition. - `deployment.rollout.mark-unknown`: records uncertain delivery/execution outcome without inferring failure or success. - `deployment.observation.report`: active Gateway-credential scope and target binding, exact decoding, idempotency, and edge evidence projection. - `deployment.rollout.get`: Tenant/Project-scoped query. The memory repository atomically stores aggregate, idempotency, audit, and outbox evidence and proves optimistic version conflicts. It is a conformance adapter, not production durability. ## Planned expansion Target snapshots, cohorts, canary/batch limits, pause gates, per-target partial success, health policy, rollout scheduling, and large evidence storage build on this single-target aggregate. Production CloudLink sends only versioned Desired offers or immutable references through application-owned outbox records; it may not write an edge cache, SHM, point, or device register. Read [Artifact registry](/en/aethercloud/concepts/artifact-registry) to understand how immutable revisions are published before a deployment references them. --- # Edge, AetherCloud, and provider authority > Authoritative source: [AetherCloud](https://github.com/EvanL1/AetherCloud/blob/main/docs/concepts/edge-cloud-boundary.md). This page is mirrored into the unified AetherIoT documentation. Wide-area connectivity and provider APIs are unreliable coordination paths, not control buses. For every cross-boundary feature, identify the authority, the projection, and the behavior during disconnection before choosing a transport. | Concern | Authority | Remote representation | | -------------------------- | ----------------------- | -------------------------------------------- | | Current point value | AetherEdge SHM | Time-stamped telemetry projection | | Device acquisition | AetherEdge runtime | Connection and health observation | | Local safety and rules | AetherEdge runtime | Versioned configuration and audit projection | | Commissioned channel state | AetherEdge local store | Desired/applied revision status | | Tenant and user access | AetherCloud | Bounded grants delivered to an edge | | Fleet membership | AetherCloud | Enrolled gateway identity | | Published artifact | AetherCloud | Verified local artifact cache | | Job intent | AetherCloud | Locally validated accepted/rejected job | | Physical command result | AetherEdge runtime | Receipt and audit record | | Desired cloud placement | AetherCloud | Provider-scoped deployment plan | | Actual cloud resource | Infrastructure provider | Normalized inventory projection | | Infrastructure state | Stack remote backend | Metadata and version reference | | Provider capabilities | Infrastructure provider | Versioned adapter capability projection | ## Desired and applied are separate facts A deployment request changes cloud desired state. It does not prove that an edge downloaded, validated, activated, or retained the artifact. The edge reports an applied revision with evidence, and the cloud records both values without silently reconciling them. ## Jobs, not remote procedure calls Work that crosses the boundary carries at least: - a globally unique job identity - tenant, gateway, and capability identity - typed arguments and contract version - issue and expiry time - idempotency key and retry policy - required permission, risk, and confirmation evidence - desired revision or other precondition The edge returns a state transition such as accepted, rejected, expired, running, succeeded, or failed. A network timeout means the result is unknown; it is not permission to repeat a non-idempotent physical action. Infrastructure work follows the same job semantics. Planning is a query-like, read-only operation; apply and destroy are commands with explicit permissions, saved-plan evidence, idempotency controls, confirmation policy, and audit. ## Disconnection behavior An edge durably buffers bounded uplink data, continues local behavior, and reconnects with an explicit resume position. The cloud tolerates duplicate delivery and acknowledges only data it has durably accepted. Retention overflow must be observable rather than hidden. ## AI boundary An AI agent may explain observed divergence or propose a deployment plan. It cannot redefine which side is authoritative. Any future command exposed to an agent uses the same governed job flow as a human or API client. --- # Gateway identity and enrollment > Authoritative source: [AetherCloud](https://github.com/EvanL1/AetherCloud/blob/main/docs/concepts/gateway-identity-and-enrollment.md). This page is mirrored into the unified AetherIoT documentation. A Gateway is AetherCloud's tenant-scoped identity for one AetherEdge runtime. It is not a device session, a live-state owner, or a certificate record. Enrollment progressively binds a registered cloud resource to proof held by the runtime. ## Implemented foundation The current TypeScript foundation implements: - runtime-validated, branded `TenantId`, `ProjectId`, and `GatewayId` values - immutable Gateway enrollment transitions `registered -> awaiting-claim -> claimed` - separate `RegisterGateway`, `IssueGatewayEnrollment`, and `ClaimGatewayEnrollment` application commands - a tenant-scoped `GetGatewayEnrollment` query - command definitions containing permission, risk, confirmation, idempotency, expiry, audit, and authorization policy - typed application failures and runtime decoding of untrusted inputs - optimistic `GatewayIdentityRepository` insert and replace operations - mutation evidence carrying authenticated actor, command governance, and integration event identity - typed storage-unavailable outcomes for read and write paths - an in-memory repository and token service for conformance and local tests - a PostgreSQL repository, explicit migration, Tenant RLS, Node `pg` pool boundary, optimistic revision checks, and atomic Gateway/Audit/Outbox writes - an opt-in PostgreSQL 18 integration test with a constrained application role These are application and adapter contracts. No fleet HTTP route, CloudLink message, production database composition/migration runner, CA, or KMS integration is implemented. The SQL adapter is executable but not deployed. ## Command boundary | Capability | Authorization | Risk | Confirmation | Idempotency and expiry | | -------------------------------- | -------------------------------------------------- | ------ | ------------ | ---------------------- | | `fleet.gateway.register` | `fleet.gateway.create` in Tenant context | low | not required | required | | `fleet.gateway.enrollment.issue` | `fleet.gateway.enrollment.issue` in Tenant context | high | explicit | required | | `fleet.gateway.enrollment.claim` | bound enrollment token | medium | not required | required | All three commands require audit by policy. They are intentionally not exposed through the HTTP composition root until authenticated Tenant context, transactional persistence, and durable audit are available. The issue command returns the raw enrollment token once. An identical retry returns only public Gateway and claim state. A retry with the same idempotency key and different input fails. The Gateway aggregate stores the token digest, never the raw token. The claim command compares token material through an application-owned token service. It binds a credential-request fingerprint but does not issue or activate a certificate. A valid duplicate claim with the same request and fingerprint is a replay; a different claimant cannot rebind the Gateway. ## Query boundary `fleet.gateway.enrollment.get` requires `fleet.gateway.enrollment.read`. The query uses Tenant and Project scope from the authenticated context and returns `gateway-not-found` outside that scope. It never returns a token, token digest, or command idempotency key. ## Implemented failures - `invalid-input` - `permission-denied` - `confirmation-required` - `command-expired` - `idempotency-conflict` - `gateway-already-exists` - `gateway-not-found` - `gateway-storage-unavailable` - `invalid-enrollment-token` - `enrollment-claim-expired` - `invalid-gateway-enrollment-transition` - `concurrent-modification` Transport error envelopes remain planned. These codes are currently library results, not published HTTP status mappings. ## Planned lifecycle The production lifecycle extends the foundation without changing its authority: ```text registered -> awaiting-claim -> claimed -> credential-pending -> active | | | expired failed suspended | revoked | recovery-pending | active (new generation) ``` Claims are short-lived and replaceable; active credentials are separately versioned. Revocation permanently fences a credential generation. Recovery requires explicit authorization and creates a new generation rather than reactivating revoked material. The [IoT Cloud roadmap](/en/aethercloud/guides/iot-cloud-roadmap) identifies which credential, persistence, and public API capabilities are available now and which remain planned. --- # Governed capability Jobs > Authoritative source: [AetherCloud](https://github.com/EvanL1/AetherCloud/blob/main/docs/concepts/governed-capability-jobs.md). This page is mirrored into the unified AetherIoT documentation. The transport-neutral domain/application foundation and atomic memory adapter are implemented. An authorized Tenant actor can create work only for a declared Gateway capability, inspect its governance metadata, confirm it when required, queue and offer it, request cancellation, mark an ambiguous outcome unknown, ingest authenticated edge Receipts, and query the resulting Job. This is not an end-to-end remote execution product. PostgreSQL, production audit/outbox, Runtime Manifest catalog wiring, CloudLink envelopes and mailbox, large evidence storage, public HTTP, scheduling workers, and the AetherEdge counterpart remain planned. ## Authority and admission - AetherCloud owns Job intent, Tenant authorization, confirmation evidence, expiry, and delivery policy. - AetherEdge owns capability acceptance, local precondition and safety checks, execution, and result facts. - A declared capability is evidence of availability, not authorization. Job creation requires both `edge.job.create` and the declaration's permission. - The capability declaration, rather than caller input, supplies risk, confirmation, replay safety, permission, and physical-effect metadata. - An unknown or malformed capability fails closed. There is no generic method name or arbitrary RPC fallback. - A physical-effect capability remains deny by default and the edge always retains its final decision. The current memory catalog is a conformance fixture. Production eligibility must consume a freshness-labelled Runtime Manifest projection and preserve the exact declaration version used for admission. ## Lifecycle and uncertainty ```text awaiting-confirmation -> authorized -> queued -> offered edge: accepted -> running -> terminal \-> unknown offered/running/unknown -> cancel-requested -> edge terminal Receipt ``` `succeeded`, `failed`, `partial`, `rejected`, `expired`, and `canceled` are terminal edge Receipt outcomes. Cloud-side expiry before edge acceptance does not fabricate a Receipt. Cancellation records intent; a later `succeeded` Receipt remains authoritative if the effect already occurred. After a network timeout the Job becomes `unknown`. In particular, an unsafe or physical-effect Job is not automatically retried under a new identity. A late authenticated Receipt may resolve uncertainty without erasing the earlier timeout or cancellation evidence. ## Receipt ordering and idempotency Receipt identity and sequence are independent replay guards. Sequence is a lossless canonical `uint64` decimal string. Exact replay returns the existing fact. Reusing an identity or sequence with different content fails closed. An out-of-order Receipt is retained as `pending-predecessor`; it does not move the projection. Filling the gap applies all newly contiguous Receipts in order. Execution terminal Receipts require an evidence digest. Large evidence bytes will live in object storage; the Job ledger stores only governed references and digests. ## Implemented application surface - `edge.job.create`: validates exact external input, the command time window, Tenant scope, platform permission, and capability permission. - `edge.job.confirm`: high risk and explicitly confirmed. - `edge.job.queue` and `edge.job.offer`: dispatch controls with independent command metadata. - `edge.job.mark-unknown` and `edge.job.cancel-request`: record cloud knowledge and intent without claiming an edge result. - `edge.job.receipt.ingest`: derives Tenant, Project, and Gateway from an active credential and enforces target binding. - `edge.job.get`: Tenant/Project-scoped query exposing governance before state. The memory adapter atomically stores Job state, idempotency, required audit evidence, and an outbox record. It proves behavior without an external service; it does not satisfy production durability. ## Planned production completion Production work adds a PostgreSQL ledger and Receipt inbox, a transactionally coupled outbox/audit chain, Runtime Manifest declaration provenance, application-owned CloudLink delivery, evidence objects, scheduling and expiry workers, public interfaces, and a reviewed AetherEdge contract. Acknowledgement to the edge occurs only after durable Receipt acceptance. Read [CloudLink reliability and lifecycle](/en/aethercloud/concepts/cloudlink-and-core-state-machines) to understand delivery, timeout, retry, and unknown-result behavior. --- # IoT Cloud capability map > Authoritative source: [AetherCloud](https://github.com/EvanL1/AetherCloud/blob/main/docs/concepts/iot-cloud-capability-map.md). This page is mirrored into the unified AetherIoT documentation. AetherCloud is an IoT fleet, data, artifact, and governed-work control plane. Multi-cloud infrastructure fusion is an adjacent capability, not the organizing model for these IoT contexts. AetherCloud coordinates unreliable wide-area work without joining the hard real-time control loop. ## Current implementation audit ### Implemented - Node.js 24, TypeScript 5.9, ESM, pnpm workspace, strict type checking, linting, formatting, tests, and coverage gates - edge/cloud/provider authority values and tests - capability-driven cloud Provider descriptors, catalog, Cloud Connection and read-only discovery foundations, plus memory conformance infrastructure - one HTTP composition root with public health/platform routes and authenticated audit JSON/finite resumable SSE query routes - agent documentation index, manifest, Skill, invariants, ADRs, and contract tests - Gateway identity and enrollment domain/application foundation described in [Gateway identity and enrollment](/en/aethercloud/concepts/gateway-identity-and-enrollment) - transport-neutral CloudLink session/heartbeat domain/application foundation with epoch fencing, cursor resume, credential verification port, and memory adapter - AetherEdge Runtime Manifest v1 report/query domain/application foundation with canonical checksum verification, monotonic history, and memory adapter - replay-safe IoT telemetry batch ingest/history domain/application foundation with lossless positions, conflict/gap/reorder handling, and atomic memory inbox/history/audit/outbox evidence - edge-authoritative alarm fact ingestion and cloud workflow projection foundation with generation/sequence ordering, visible gaps, cloud-only acknowledgement, and an atomic memory adapter - optional OpenTelemetry adapter foundation with default no-op operation, in-memory and OTLP modes, bounded export, W3C context extraction, low-cardinality ingestion signals, and exporter-failure isolation - immutable Artifact Revision publication/query foundation with digest/content verification, signature-verifier abstraction, compatibility metadata, release-channel conflict protection, and atomic memory audit/outbox evidence - single-target Desired/Reported/Applied deployment foundation with published Artifact preconditions, lossless generations, pause/resume/cancel/rollback, explicit unknown outcome, authenticated edge observations, and memory audit/outbox - governed capability Job foundation with declaration-derived permission/risk, explicit confirmation, queue/offer controls, unknown and cancellation intent, ordered authenticated Receipts, and atomic memory audit/outbox evidence - Tenant/Project-scoped audit search, webhook subscription, bounded delivery/retry/dead-letter/redrive, and asynchronous data-export foundations with memory adapters; audit search is exposed through HTTP/SSE - transport-neutral MCP capability/Audit resources and governed Data Export/Job tools that invoke the same application use cases ### Designed or planned, not implemented - Tenant, User, Service Account, RBAC/ABAC, API credentials, and durable audit - Site, Instance, Point, topology, and dynamic groups beyond the Runtime Manifest/capability foundation - CloudLink executable process, protocol messages, production credentials, PostgreSQL session/cursors, durable inbox/outbox, and backpressure - production telemetry/alarm PostgreSQL persistence, CloudLink wire adapters, public history/search APIs, downsampling, export, assignment, and comments - production artifact stores/upload/API and multi-target deployment scheduling, persistence, wire, and public interfaces - production governed Job ledger/inbox, CloudLink delivery, Runtime Manifest declaration provenance, workers, public interfaces, and AetherEdge counterpart - PostgreSQL, object-storage, time-series, durable outbox, integration/export workers, production webhook sender/destination secrets/signing, live SSE, WebSocket, MCP wire/root and remaining tools, fleet operations, quota, or cost adapters - a production Gateway CA, KMS token service, certificate revocation, recovery, or enrollment HTTP endpoint The planned items below are contracts and delivery direction, not available endpoints or protocol messages. The [IoT Cloud roadmap](/en/aethercloud/guides/iot-cloud-roadmap) distinguishes executable capabilities from missing production surfaces. ## Capability map | Bounded context | User value and core aggregates | Authority | Queries / commands | Storage needs | AetherEdge interaction | Main safety risk | Phase | | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | | Identity and Access | Isolate and delegate access with `Tenant`, `Project`, `User`, `ServiceAccount`, `Role`, `Grant`, `Policy`, and `ApiCredential` | AetherCloud owns membership and authorization; an external IdP may prove login identity | Q: principals, grants, effective permissions. C: invite, grant, revoke, rotate credential | PostgreSQL; hashes or secret references only; audit and outbox in the same transaction | Deliver bounded authorization evidence; edge still applies local policy | forged Tenant context, cross-tenant IDOR, over-broad service accounts, leaked keys | minimum actor context begins in phase 1; durable IAM before public Tenant APIs | | Fleet Identity and Enrollment | Establish and recover trusted runtime identity with `Site`, `GatewayIdentity`, `EnrollmentClaim`, `CredentialBinding`, and `RecoveryAttempt` | AetherCloud owns fleet membership; Gateway owns its private key; issuer owns certificate validity | Q: Gateway and enrollment status. C: register, issue claim, claim, suspend, revoke, recover | memory conformance now; PostgreSQL and secret/KMS-backed token service later | one-time bootstrap claim, then credential-bound CloudLink authentication | token used as a long-term credential, replay, cross-tenant recovery, revoked credential resurrection | phase 1 | | Fleet Topology and Runtime Catalog | Search sites and understand runtime compatibility with `Site`, `Gateway`, `Instance`, `PointMetadata`, `Group`, `TopologyEdge`, `RuntimeManifest`, and `CapabilityCatalog` | cloud owns grouping; edge reports actual instances, points, runtime, and supported capabilities | Q: topology, dynamic groups, version distribution. C: edit site/group/tag; authenticated manifest ingestion | PostgreSQL for metadata/latest projection; object storage for large historical manifests if required | edge sends versioned manifest and capability snapshots | generic relation model erases lifecycles; stale manifest treated as current; cloud rewrites commissioned channels | phase 3 | | CloudLink Session and Delivery | Maintain resumable, observable cloud-edge connectivity with `Session`, `SessionEpoch`, `ProtocolNegotiation`, `StreamCursor`, `Inbox`, and `OutboundMailbox` | cloud owns durable cursors and connection projection; edge owns its unacknowledged bounded buffer | Q: session, lag, version. C: authenticate, negotiate, resume, heartbeat, durable ingest; never arbitrary RPC | PostgreSQL session/cursor/inbox/outbox; active socket only in CloudLink memory | version negotiation, per-stream order, acknowledgement, reconnect, and flow control | split-brain sessions, acknowledge-before-durable, gaps, unsafe int64 conversion, unbounded queues | phase 2 | | IoT Telemetry Projection | Query replay-safe Point and event history with `TelemetryStream`, `TelemetryBatch`, `PointSample`, `DeviceEvent`, `IngestionReceipt`, and `RetentionClass` | edge owns live values; cloud owns durable historical facts, retention, and freshness-labelled projections | Q: history, aggregates, cursor/gap. C: atomically ingest batch and set retention | PostgreSQL metadata/inbox/cursor and initial history; object storage raw/cold batches; replaceable analytics store | edge replays stream positions; cloud acknowledges only after durable acceptance | history represented as live state, silent gaps, conflicting replay, unsafe int64 conversion | phase 4 | | Alarm Projection | Search edge alarm facts and manage cloud workflow with `AlarmOccurrence`, `AlarmProjection`, `Acknowledgement`, `Assignment`, and `Comment` | edge owns alarm facts; cloud owns replay-safe projection and operator workflow overlays | Q: alarm search/timeline. C: ingest facts and change cloud-only workflow; cloud cannot clear edge alarm | PostgreSQL inbox, alarm facts, projections, workflow, audit, and outbox | edge replays alarm generation/sequence; cloud preserves stale/gap state | cloud acknowledgement confused with edge clear; late clear loses predecessor; disconnect clears alarm | phase 5 | | Artifact Registry and Release | Publish verifiable immutable Pack, configuration, model, rule, and application revisions with `Artifact`, `Revision`, `Digest`, `Signature`, `Compatibility`, and `Channel` | AetherCloud owns publication and channel references; edge owns verified local cache | Q: metadata, compatibility, provenance. C: upload, validate, sign, publish, deprecate, withdraw, move channel | PostgreSQL metadata; object storage blobs, signatures, SBOM, provenance | edge downloads by digest and validates before local acceptance | mutable blob, supply-chain injection, missing compatibility, evidence deletion | phase 6 | | Deployment and Desired State | Roll out, pause, resume, and roll back artifacts with `Deployment`, `Rollout`, `Target`, `DesiredGeneration`, `ReportedRevision`, and `AppliedEvidence` | cloud owns desired generation and rollout; edge owns reported and applied facts | Q: desired/reported/applied divergence. C: start, pause, resume, cancel request, rollback | PostgreSQL transaction state and outbox; object storage for large evidence | send immutable revision reference; edge accepts/rejects/downloads/validates/applies and reports | download inferred as applied, late report rolls projection back, partial failure hidden | phase 7 | | Governed Capability Jobs | Safely request diagnostics and later controlled work with `CapabilityDeclaration`, `Job`, `Confirmation`, `Precondition`, `Attempt`, and `Receipt` | cloud owns intent/policy/confirmation; edge owns accept/reject/expire/execute/result | Q: Job and Receipt. C: create, confirm, offer, request cancellation; every command has governance metadata | PostgreSQL ledger/audit/inbox/outbox; object storage for large evidence | only versioned capability Jobs; never direct point, SHM, or register writes | blind retry after unknown physical result, arbitrary RPC catalog, stale preconditions, confirmation bypass | phase 8 | | Audit, Integration, and Delivery | Trace actions and deliver data with `AuditEvent`, `Subscription`, `WebhookEndpoint`, `ExportJob`, `DeliveryAttempt`, and `DeadLetter` | cloud owns its audit and delivery state; consumers own downstream processing | Q: audit and delivery status. C: create subscription/webhook/export, redrive dead letter | PostgreSQL audit/outbox/delivery; object storage exports; broker only after evidence | consumes application events after CloudLink ingestion, never sockets directly | SSRF, weak webhook signatures, PII leak, duplicate delivery, audit split from business commit | audit metadata starts phase 1; durable integration phase 9 | | Operations and Cost Guardrails | Observe fleet health, lag, versions, deployments, drift, quota, retention, and cost with `FleetHealthProjection`, `Backlog`, `VersionDistribution`, `Quota`, and `RetentionBudget` | each source fact keeps its authority; operational views are freshness-labelled projections; cloud owns quota policy | Q: health, lag, version, drift, cost. C: set quota, rate, and retention policy | PostgreSQL configuration/summary and analytics | combines heartbeat, manifest, ingestion lag, deployment, and Job evidence | stale health presented as truth or quota causes silent loss | incremental; unified phase 9 | | Operational Observability | Diagnose API, CloudLink, ingestion, database, worker, and delivery behavior with traces, low-cardinality metrics, and correlated structured logs | each product context owns its facts; OpenTelemetry signals are sampled operational evidence only | no business command/query; composition roots configure no-op or OTLP adapters | optional observability backend through OTLP; no transactional or audit role | optional W3C Trace Context only; baggage is never authorization | high-cardinality or secret leakage; exporter failure changes business result; trace mistaken for audit | adapter foundation begins phase 2 and expands through phase 9 | | AI-native Contracts and MCP | Let agents discover truth and safely call existing capabilities with `DocsManifest`, `CapabilityDefinition`, permission/error/event schemas, and golden paths | versioned repository contracts and application capability definitions are authoritative; AI output is not | Q: docs, capability metadata, projections. C: MCP tools only proxy implemented application commands | Git Markdown and manifest; runtime calls use normal audit persistence | no alternate CloudLink or control path | planned feature presented as callable, tool omits risk/confirmation, agent bypasses authorization | documentation every phase; MCP phase 10 | | Provider and Infrastructure Fusion | Place cloud workloads through the capability-driven Provider Adapter registry | each provider owns actual resource state; AetherCloud owns governed placement intent | outside this IoT capability expansion | independent deployment-stack State | related only through placement and governed infrastructure Jobs | IoT core gains a fixed vendor enum or global State | adjacent existing track | ## Cross-context dependencies ```text Identity and Access -> authorization decisions Fleet Identity -> CloudLink credential verification and session fencing Runtime Catalog -> artifact compatibility and Job eligibility Artifact Registry -> immutable deployment revision references CloudLink adapter -> telemetry/deployment/Job application ingestion All command contexts -> audit + transactional outbox Outbox -> projections, notifications, webhooks, exports, and SSE/WebSocket ``` Contexts exchange deliberate contracts or events, not internal database rows. Shared code is limited to stable identities, time, actor, correlation, and capability-definition values. ## Governance, failure, and implementation supplement The capability table above carries user value, aggregates, authority, use cases, storage, edge interaction, risk, and phase. This supplement makes the permission, command policy, offline result, and current executable layer explicit. Exact names and layers are machine-readable in [`ai/application-contracts.json`](https://github.com/EvanL1/AetherCloud/blob/main/ai/application-contracts.json). | Context | Representative permission | Command risk / confirmation | Offline, retry, or failure result | Current status | | ------------------------- | ----------------------------------------------------------------------------------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | Identity and Access | `identity.grant.manage` | high / explicit | authorization fails closed; credential rotation is idempotent | planned | | Fleet Identity | `fleet.gateway.create`, `fleet.gateway.enrollment.issue` | low or high / none or explicit | expired token, invalid token, idempotency conflict, concurrent modification | domain/application/memory enrollment foundation implemented; production surface planned | | Fleet Runtime Catalog | `fleet.runtime-manifest.report`, `fleet.runtime-manifest.read` | low / none | old generation retained; checksum or generation conflict fails closed | domain/application/memory manifest foundation implemented; production surface planned | | CloudLink | `cloudlink.session.open`, `cloudlink.session.heartbeat` | low / none | rejected version, fenced epoch, stale heartbeat, reconnect resume | domain/application/memory session foundation implemented; wire/root/durability planned | | IoT Telemetry | `telemetry.batch.ingest`, `telemetry.history.query` | low / none | duplicate returns receipt; conflict quarantines; gap requests replay | partial: domain/application/memory inbox-history-outbox and OTel decorator implemented | | Alarm Projection | `alarm.fact.ingest`, `alarm.workflow.acknowledge` | low / none | duplicate fact is safe; gap/stale remain visible | partial: domain/application/memory projection and workflow implemented | | Artifact Registry | `artifact.revision.publish`, `artifact.revision.read` | high / explicit | conflicting digest, revision/channel race, or signature failure | partial: domain/application/memory content-signature-metadata adapter implemented; production stores/API planned | | Deployment | `deployment.rollout.start`, control, observation, and read | high / explicit for start/rollback | disconnect becomes unknown; late evidence reconciles; cancellation is intent | partial: single-target domain/application/memory audit-outbox foundation implemented; target scheduler/wire/storage/API planned | | Governed Jobs | capability-specific plus `edge.job.create` | declared per capability / explicit when required | edge reject/expire, timeout unknown, late Receipt resolution | partial: domain/application/memory audit-outbox foundation implemented; production ledger/delivery/wire/API planned | | Audit and Integration | `audit.event.read`, `integration.webhook.subscription.create`, `data.export.create` | high / explicit for subscription, export, or redrive | delivery retry, visible dead letter, idempotent redrive, immutable export reference | partial: audit HTTP/SSE plus domain/application/memory subscription, delivery, and export foundations; production durability/sending/workers planned | | Operations | `operations.read`, `operations.quota.manage` | medium / explicit for quota reduction | stale projection and quota rejection are explicit | planned | | Operational Observability | deployment configuration only | not a product command | bounded signal loss; never changes business outcome | optional adapter and telemetry decorator implemented; broader root instrumentation/deployment planned | | AI-native/MCP | same permission as underlying use case | same risk and confirmation as underlying command | planned capability is rejected, never simulated | partial: capability/Audit resources and Data Export/Job tool interface implemented; wire/root, production identity, and remaining exposure planned | Read [Architecture](/en/aethercloud/concepts/architecture) for process and persistence boundaries, [CloudLink and core state machines](/en/aethercloud/concepts/cloudlink-and-core-state-machines) for failure semantics, and the [delivery roadmap](/en/aethercloud/guides/iot-cloud-roadmap) for executable slices. --- # IoT business telemetry > Authoritative source: [AetherCloud](https://github.com/EvanL1/AetherCloud/blob/main/docs/concepts/iot-telemetry.md). This page is mirrored into the unified AetherIoT documentation. The protocol-neutral domain, application ingest/history use cases, canonical batch digest, in-memory conformance adapter, experimental CloudLink MQTT point-batch mapping, and PostgreSQL telemetry adapter are implemented. The PostgreSQL slice atomically stores replay identity, stream/cursor state, accepted business facts, Audit, the integration Outbox, and an exact durable ACK outbox. It proves pre-commit rollback and post-commit identical-ACK recovery against PostgreSQL 18. Public HTTP history, production database composition, joint production authentication, multi-sample mapping, and durable data-loss persistence remain planned; the memory adapter is not a production durability claim. IoT business telemetry is Tenant-owned product data. It is not OpenTelemetry operational data. An AetherEdge runtime remains authoritative for the current live point value; AetherCloud stores replay-safe, time-stamped historical facts and freshness-labelled projections. ## Domain boundary The first slice uses explicit resources rather than a generic entity table: - `TelemetryStream` identifies one ordered logical uplink stream for a Gateway. - `StreamEpoch` fences sequence reuse after a deliberate stream reset. - `StreamPosition` is an unsigned 64-bit value encoded as a canonical decimal string at JSON boundaries and represented losslessly in TypeScript. - `TelemetryBatch` is the atomic ingestion unit and owns a bounded ordered list of records. - `PointSample` carries an Instance/acquisition-Point identity, source time, typed value, and source quality. Only `telemetry` and `status` Point kinds are accepted; command/action kinds cannot become a control backdoor. - `TelemetryTopologyBinding` preserves the edge publication epoch and an algorithm-qualified topology snapshot digest for each batch. - A Thing Model reference is optional and must come from commissioned evidence; AetherCloud does not invent one for an edge-native Point. - `DeviceEvent` carries a versioned event type, source time, and bounded typed payload. - `IngestionReceipt` records the batch identity, digest, durable cursor, acceptance status, and cloud persistence time. - `RetentionClass` selects an authorized retention policy; callers do not provide arbitrary storage duration. AetherEdge currently exposes numeric acquired samples and four quality states. The cloud value model may be broader for future thing models, but the first wire contract must not claim edge value variants that the edge cannot emit. ## Trusted identity and scope CloudLink resolves Tenant, Project, Gateway, and credential generation from an authenticated active Gateway credential before calling the ingestion use case. An envelope may repeat those identifiers for correlation, but payload fields are never authorization evidence. Suspended or revoked credential generations fail closed. The application command is `telemetry.batch.ingest`. It requires a Gateway credential scope, expiry, idempotency, audit, bounded size, and an application- owned repository/inbox transaction. HTTP, a test fixture, or a future alternate transport must invoke the same command rather than writing history directly. ## Batch identity, digest, and atomicity The implemented stable identity is: ```text TenantId + ProjectId + GatewayId + StreamId + StreamEpoch + first position ``` The canonical payload digest covers versioned business content, not transport headers, trace context, retry count, socket identity, or compression bytes. The MVP accepts a complete batch atomically. Validation, limits, authorization, record uniqueness, digest calculation, sequence evaluation, inbox identity, business facts, required audit, and outbox events succeed in one transaction or none become accepted. A future partial-record mode requires a new contract version and per-record receipts. ## Duplicate, conflict, gap, and reorder behavior - The same identity and digest is a duplicate. It returns the prior durable acknowledgement without creating history or integration events again. - The same identity with different content is a conflicting duplicate. It is quarantined, audited, and never acknowledged as accepted. - The next contiguous batch advances the durable cursor after transaction commit. - A forward gap is durably observable and requests replay. It does not advance the contiguous acknowledgement cursor. - A bounded reorder window may retain later batches as pending. Unbounded buffering is forbidden. - A batch older than the durable cursor is either a verified duplicate or a conflict; age alone never proves equality. - Retention overflow at the edge becomes an explicit loss/gap marker. Neither side fabricates missing samples. ## Durable acknowledgement `received`, `decoded`, `validated`, `accepted`, `durably persisted`, and `acknowledged` are distinct observations. CloudLink may send a successful durable acknowledgement only after the transaction containing de-duplication identity and accepted business facts commits. If the connection fails before the Gateway receives that acknowledgement, the Gateway safely replays the batch. A crash after commit returns the same receipt on replay. A storage timeout with an unknown commit result must be resolved by the idempotency identity before any new acceptance is attempted. The implemented PostgreSQL path stores the exact alpha.3 ACK projection in the same transaction. A bounded delivery worker leases pending rows with `FOR UPDATE SKIP LOCKED`, publishes sequentially, marks delivery only after a successful publish, and releases failures for bounded retry. Replay requeues the same event ID, receipt ID, persisted time, stream position, batch ID, and digest; it never invents a second business fact. The application validates all claimed rows at the adapter boundary before publishing them. ## Time and freshness Every record keeps at least: - edge/source event time - cloud received time - cloud persisted time - stream epoch and position - source quality History queries expose source and ingest time. A latest-value projection also exposes its durable position, freshness, and known gap state. It is never labelled as the current physical value. Late data is retained in event-time history but cannot silently roll a latest projection backward. ## Storage responsibilities `PostgresTelemetryRepository` now owns the first bounded implementation of stream metadata, durable inbox identities, contiguous cursors, gap state, retention policy, bounded history, Audit, integration Outbox, and CloudLink ACK outbox. Tenant-scoped keys and forced Row-Level Security are defense in depth; the normal application role remains non-superuser and non-`BYPASSRLS`. Object storage owns content-addressed raw/cold batches and exports when their size justifies it. A future time-series or analytics adapter owns large history, aggregation, and downsampling. Neither store becomes live-state authority. The history port is owned by the telemetry application context so a measured ingestion hot path can move without changing the domain contract. ## Resource and security limits The decoder rejects unsupported contract versions, unsafe integer encodings, non-finite numeric samples, unknown record variants, duplicate positions inside a batch, excessive records, oversized values, decompression expansion, and unbounded metadata. Quota rejection and backpressure are visible results rather than silent loss. TenantId, ProjectId, GatewayId, PointId, raw payloads, and arbitrary event type strings are forbidden as OpenTelemetry metric labels. Enrollment tokens, credentials, private material, and authorization headers never enter telemetry payloads, errors, logs, traces, or audit details. ## Application surface and status - Command `telemetry.batch.ingest`: domain/application, memory, and PostgreSQL adapters implemented. - Query `telemetry.history.query`: application, memory, and bounded PostgreSQL adapters implemented. - Event `telemetry.batch-accepted.v1`: memory and PostgreSQL transactional Outbox behavior implemented. - Exact CloudLink durable ACK outbox and bounded delivery use case: implemented for accepted telemetry; production worker composition is planned. - Stream cursor, gap, lag, and retention-state query: planned. - CloudLink MQTT adapter: experimental codec/bridge/ingress and the shared alpha.3 AetherEdge fixture manifest implemented; the bridge consumes the persisted ACK projection, while production database/worker composition and session persistence remain planned. - HTTP history adapter: planned. Read [operational observability](/en/aethercloud/concepts/operational-observability) to understand why traces, metrics, and logs remain separate from durable IoT telemetry. --- # MCP application interface > Authoritative source: [AetherCloud](https://github.com/EvanL1/AetherCloud/blob/main/docs/concepts/mcp-application-interface.md). This page is mirrored into the unified AetherIoT documentation. The MCP interface is an adapter over AetherCloud application commands and queries. It does not own business state, relax authorization, manufacture a planned capability, or connect directly to PostgreSQL, an outbox, CloudLink, or an edge runtime. ## Implemented interface foundation `apps/mcp` implements a transport-neutral `AetherCloudMcpInterface` with runtime decoding and typed results: - `listResources()` advertises `aethercloud://capabilities` and the `aethercloud://audit/events{?cursor,limit}` resource template; - the capability resource reports application status separately from MCP exposure status, including planned MCP exposure; - the Audit resource injects authenticated Tenant, Project, subject, and permissions, then invokes the same `SearchAuditEvents` query used by HTTP; - `listTools()` exposes only executable MCP adapters and includes permission, risk, confirmation, idempotency, expiry, audit, and input-schema metadata; - `data.export.request` invokes `RequestDataExport`; its high-risk explicit confirmation is still enforced by the application command; - `edge.job.create` invokes `CreateGovernedJob`; the capability declaration and edge-side decision remain authoritative for the requested work; - unknown or application-only capabilities fail with `mcp-tool-not-implemented` before any use case is invoked. Authenticated scope is supplied by the future MCP composition root, never by a tool's business `input`. Command envelopes contain confirmation, idempotency key, issue time, and expiry; the interface forwards them unchanged to the application decoder. It cannot weaken capability metadata. ## Implemented versus planned The resource/tool registry, application delegation, exact external decoding, capability-status resource, and behavior tests are implemented. There is no MCP SDK transport, stdio server, Streamable HTTP endpoint, OAuth flow, session composition root, rate limiter, or production identity/audit persistence yet. Consequently the package is an executable interface foundation, not a network server that an MCP client can connect to today. Adding a wire transport must be a thin composition root around this interface. It must translate MCP protocol errors and content without changing the underlying command/query context or reaching around the use case. Command tools remain deny by default; a tool is absent until both its application behavior and explicit MCP adapter are executable. ## Security and content boundary Resources and tools return bounded JSON content. Raw credentials, enrollment tokens, artifact bytes, webhook secrets, export bytes, SHM addresses, and device-register values are not MCP content. A data export returns only its governed resource state and immutable object metadata; download remains a separate planned authorization boundary. Read the [application contract catalog](/en/aethercloud/reference/application-contracts), [audit and integrations](/en/aethercloud/concepts/audit-and-integrations), and [governed capability Jobs](/en/aethercloud/concepts/governed-capability-jobs) before exposing another resource or tool. --- # Multi-cloud fusion > Authoritative source: [AetherCloud](https://github.com/EvanL1/AetherCloud/blob/main/docs/concepts/multi-cloud-fusion.md). This page is mirrored into the unified AetherIoT documentation. Multi-cloud fusion is AetherCloud's primary cloud capability. It gives tenants one resource graph, policy model, plan workflow, and audit trail across public clouds, private clouds, sovereign environments, and on-premises infrastructure. It does not pretend that those providers expose identical resources. ## The common layer AetherCloud owns a small, capability-driven vocabulary: - provider and cloud-connection identity - regions, zones, and placement constraints - capabilities such as compute, object storage, private networking, managed Kubernetes, managed PostgreSQL, and IoT ingress - deployment stacks, desired topology, saved plans, policy decisions, jobs, and normalized resource observations - cost, quota, health, provenance, and freshness metadata A provider advertises its available capabilities. Placement selects only a provider connection that satisfies the requested capabilities and tenant policy. Selection is explicit; credential shape is never used to infer a provider. ## Current implementation The repository currently implements the Provider descriptor, CloudConnection, ProviderRegion observation, dynamic ProviderAdapter registry, and the authorized tenant/project-scoped `DiscoverProviderRegions` application query. Inputs and adapter output are runtime-validated before they become a normalized observation. The reusable conformance suite and memory adapter make the contract executable without a cloud account. The domain also implements a DeploymentStack definition whose Tenant, Project, Provider, Connection, and State key are derived from one active CloudConnection. The State backend is an opaque remote reference; locking and encryption are mandatory rather than caller-selected options. A placement observation must match the same Provider and Connection before its region can be selected, and the Stack records the canonical observation time so later policy can reject stale placement evidence. The application now implements the governed, tenant/project-scoped `PlanDeploymentStack` command, dynamic OpenTofu/Terraform `InfrastructureEngine` registry, runtime engine-output validation, change summary, policy receipt, and idempotent Plan repository port. A shared conformance suite, memory engine, and memory Plan repository make the contract executable without a cloud account. A real OpenTofu CLI adapter dynamically probes its version, verifies immutable source artifacts, runs an argv-only saved Plan in a bounded temporary workspace, parses versioned JSON, stores the Plan through an encrypted-artifact port, and proves an injected State-lock lease was released. The port has no Apply operation. Real provider SDK adapters, credential resolution, connection and DeploymentStack persistence, inventory APIs, placement policy, production remote State and distributed locking, encrypted object storage, a sandboxed worker deployment, a Terraform CLI adapter, and Apply remain planned. Neither discovery nor Plan has an HTTP route yet. Follow [Add a Provider Adapter](/en/aethercloud/guides/add-provider-adapter) for discovery and [Plan infrastructure safely](/en/aethercloud/guides/plan-infrastructure) for engine work instead of inventing new boundaries. ## The provider-specific layer Each Provider Adapter owns: - authentication and short-lived credentials - region, zone, quota, SKU, and price discovery - provider-specific infrastructure modules - provider-native capability extensions - conversion from engine JSON and provider APIs into normalized observations - conformance fixtures for provider-specific discovery and normalization Namespaced extensions preserve capabilities that do not belong in the common model. Portability means that intent and policy can be compared; it does not mean hiding every provider feature behind a false universal API. ## Infrastructure execution OpenTofu is the default engine. Terraform is supported through the same `InfrastructureEngine` port. The implemented application flow selects an immutable versioned provider module and topology, asks an engine for a saved Plan, validates versioned JSON-derived changes and State lock evidence, then evaluates policy and records an encrypted artifact reference. The real local OpenTofu CLI worker is implemented; its production remote State, credential, artifact-store, and worker-deployment adapters and every Apply path are planned. ```text DesiredTopology ↓ PlacementPolicy ↓ Provider Adapter + versioned module ↓ OpenTofu/Terraform plan -out ↓ show -json → validation, summary, policy ↓ encrypted saved Plan receipt future separate workflow: approved exact Plan → Apply → refresh → NormalizedResourceProjection ``` The infrastructure engine calculates dependencies and proposed drift. AetherCloud owns workflow, permissions, policy, audit, and the normalized resource graph. A policy-approved Plan is not approval evidence and cannot be applied through the current port. Direct provider SDKs complement the engine for discovery, pricing, quotas, temporary credentials, and operations that are not infrastructure lifecycle changes. ## State isolation One State belongs to one deployment stack. A stack belongs to exactly one tenant, provider connection, account or subscription, and primary region. Cross-region modules may exist where a provider supports them, but one State never spans multiple provider connections and is never tenant-global. State uses a remote backend with locking, encryption, versioning, backup, and retention. The implemented Plan contract requires exact State-key correlation and acquired-and-released lock evidence. Durable lineage, serial, provider versions, backup, and retention remain planned. Workers remain stateless and never treat a local workspace as durable storage. ## Credentials Cloud connections contain metadata and a secret reference, not plaintext credentials. Workers prefer workload identity or short-lived credentials, receive them only for the job, inject them through the provider's supported environment or credential mechanism, and destroy the workspace afterward. Credentials must not appear in generated modules, saved plans, logs, prompts, or normalized resource records. ## Multi-cloud operations Fusion occurs above individual State files: - inventory and search across providers - policy and placement across cost, region, sovereignty, capability, and quota - coordinated rollouts using multiple independently auditable jobs - data-routing intent across edge sites and cloud destinations - normalized health and drift views with provider-native detail available A cross-cloud rollout is a saga of provider-scoped jobs, not a distributed transaction. Partial success remains visible and compensation is explicit. ## Control-plane database portability `managed-postgresql` is a portable Provider capability, not a fixed vendor product. Future provider profiles retain native engine variants, supported versions and extensions, HA, backup, encryption, private connectivity, replication, region, sovereignty, RPO/RTO, price evidence, and namespaced extensions. Core persistence code remains one PostgreSQL adapter and never branches on a provider enum. Each AetherCloud control-plane cell has one authoritative PostgreSQL writer topology. Managing resources across many providers does not require synchronous cross-cloud database writes. Tenant home-cell placement, replica promotion, disaster recovery, and migration are explicit governed workflows. See [PostgreSQL persistence and multi-cloud cells](/en/aethercloud/concepts/persistence-and-multi-cloud-cells) for the user-visible placement and isolation model. ## Lessons retained from HPC-NOW HPC-NOW demonstrates that a useful common lifecycle can sit above separate provider IaC templates and that provider state can be normalized for a unified multi-cluster view. AetherCloud retains those ideas while replacing hard-coded vendor branches with a registry, requiring explicit provider identity, parsing versioned JSON instead of raw State text, and separating secrets from modules. --- # Operational observability > Authoritative source: [AetherCloud](https://github.com/EvanL1/AetherCloud/blob/main/docs/concepts/operational-observability.md). This page is mirrored into the unified AetherIoT documentation. OpenTelemetry is the vendor-neutral instrumentation and export boundary for AetherCloud processes. A no-op default, validated environment opt-in, OTLP HTTP trace/metric exporters, in-memory exporters, W3C Trace Context extraction, bounded span processor, low-cardinality telemetry-ingestion decorator, and failure-isolation tests are implemented. The experimental CloudLink MQTT ingress also exposes a neutral bounded observer, but OpenTelemetry wiring for its handshake/session/reconnect/ACK signals remains planned. Collector deployment and broad HTTP, database, worker, and webhook instrumentation remain planned. Operational observability answers whether AetherCloud components are healthy and where time or failures occur. It is not IoT business telemetry, a durable command receipt, an authorization source, or an audit ledger. ## Dependency boundary Domain and application packages do not import OpenTelemetry APIs or SDK types. Standard framework/database instrumentation belongs in adapters and composition roots. Application-specific operation names may be observed through a neutral application-owned observer port or a use-case decorator. The API, CloudLink, and worker roots independently configure and close their SDK lifecycle. The default is a no-op implementation. OTLP export and a Collector are optional deployment choices; tests and correct product behavior never require either one. Exporter queues and flushes are bounded. An unavailable exporter can drop operational signals after bounded retry, but it cannot block HTTP commands, CloudLink heartbeat, telemetry persistence acknowledgement, inbox/outbox work, or commissioned edge behavior. ## Initial traces The telemetry-ingestion use-case span is implemented. Planned spans also cover: - HTTP request and application use case - CloudLink authentication, negotiation, session, resume, and reconnect - telemetry decode, validation, de-duplication, persistence, and acknowledgement - inbox/outbox lease and delivery - PostgreSQL transaction - webhook delivery - background worker execution W3C Trace Context may cross CloudLink as optional metadata. Trace context and baggage never participate in message identity, payload digest, ordering, authorization, permission, risk, or Tenant resolution. Remote baggage uses a small allow-list and strict byte limits. ## Initial metrics The initial low-cardinality catalog includes: - active CloudLink sessions - handshake failures and reconnect count - heartbeat age and round-trip latency - telemetry batches and samples by bounded result class - duplicate, conflicting replay, and sequence-gap count - persistence and acknowledgement latency - inbox/outbox backlog, retry, and dead-letter count - application command/query duration and failure class - worker duration and bounded queue depth - dropped OpenTelemetry signal count Metric attributes use bounded classifications such as service, operation, protocol version, result class, and deployment environment. TenantId, ProjectId, GatewayId, PointId, JobId, user identity, token, certificate, full URL, payload, and free-form error text are forbidden metric labels because they leak sensitive context or create high-cardinality series. Standard stable HTTP, database, RPC, and messaging semantic conventions are preferred. Product-specific names use a documented `aethercloud.*` namespace and an explicit stability/version catalog. Experimental semantic conventions do not become stable AetherCloud contracts by accident. ## Logs, traces, and audit Structured application logs remain the first logging surface and carry trace/span correlation when tracing is enabled. Raw IoT payloads, tokens, authorization headers, private keys, certificate material, and arbitrary exception bodies are redacted before logging or span enrichment. Audit records are unsampled, Tenant-scoped product facts committed with the business transaction. Traces are sampled, best-effort diagnostic signals. A trace may reference an audit event identity and an audit event may retain a trace identity, but neither substitutes for the other. A successful span or export says nothing about business transaction success. ## Sampling and tests Head sampling is sufficient for the first adapter. Collector-side tail sampling may be introduced later without changing application behavior. Error and security-denial traces may receive a higher sampling probability, while the corresponding audit record remains mandatory and unsampled. Conformance tests use a no-op observer and in-memory exporters. They cover context propagation, missing context, exporter failure isolation, bounded queues, redaction, forbidden high-cardinality labels, and independence between audit persistence and trace sampling. Read [IoT business telemetry](/en/aethercloud/concepts/iot-telemetry) for the durable product data path and the boundary between business facts and operational signals. --- # PostgreSQL persistence and multi-cloud cells > Authoritative source: [AetherCloud](https://github.com/EvanL1/AetherCloud/blob/main/docs/concepts/persistence-and-multi-cloud-cells.md). This page is mirrored into the unified AetherIoT documentation. PostgreSQL is AetherCloud's default control-plane transaction engine. This does not make AetherCloud a PostgreSQL management product and does not erase its multi-cloud Provider Adapter model. ## Responsibility split | Store | Owns | Does not own | | ----------------------------- | ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | | PostgreSQL | Tenant/Fleet metadata, identity, session cursor, Inbox, Outbox, Audit, Deployment and Job ledgers | edge live state, provider resources, infrastructure State, large immutable blobs | | Object storage | artifacts, provenance, raw/cold batches, evidence, exports | mutable aggregate or authorization state | | Time-series/analytics adapter | historical telemetry scans, downsampling, aggregates after measured need | live Point authority or command transactions | | Deployment Stack backend | one provider-scoped locked infrastructure State | IoT product data or cross-provider global state | | OpenTelemetry backend | sampled operational traces and metrics | Audit, IoT telemetry, Receipt, or authorization evidence | PostgreSQL can initially implement a bounded telemetry-history port. Moving history to TimescaleDB or ClickHouse does not change CloudLink acknowledgement: the owning ingestion application contract must still establish durable replay identity and accepted business facts before acknowledging the edge. ## Multi-cloud deployment model Multi-cloud has three different meanings: 1. **Portable cell:** the same API, CloudLink, worker, PostgreSQL, and object storage composition can run in any supported environment. 2. **Multi-cloud management:** one cell manages provider-scoped resources and Gateways across many clouds. Its database need not span those clouds. 3. **Multi-cloud active-active control plane:** multiple cells write the same Tenant transaction state. This is not implemented and requires a separate consensus, fencing, conflict, latency, and recovery decision. Multi-cloud management does not require synchronous cross-cloud database writes. The default topology is an explicit Tenant home cell: ```text Global tenant directory ├── Tenant A -> Cell 1 -> PostgreSQL writer + object storage ├── Tenant B -> Cell 2 -> PostgreSQL writer + object storage └── Tenant C -> Cell 3 -> PostgreSQL writer + object storage Each cell API + CloudLink + workers | +-- one authoritative PostgreSQL writer topology ``` Cross-cloud replicas and backups may improve recovery, but they are not write authority until a governed failover fences the old cell. CloudLink durable acknowledgement never waits for an unrelated cross-cloud infrastructure saga. ## Provider database profiles Core provider descriptors can advertise `managed-postgresql`. A future profile discovery contract will retain, rather than flatten, at least: - provider-native engine/profile identity; - PostgreSQL-compatible versions and optional extensions; - regional availability, sovereignty, and private connectivity; - HA, replica, backup, restore, encryption, and maintenance capabilities; - stated RPO/RTO and observed cost evidence; - provider-specific namespaced extensions. The core repository never branches on AWS, Azure, GCP, or any other fixed vendor identifier. A provider without managed PostgreSQL may expose a governed self-hosted profile later. ## Implemented Gateway persistence slice The current executable slice includes: - `PostgresGatewayIdentityRepository` with parameterized SQL; - a real `pg` driver pool adapter behind a narrow local contract; - explicit registration, pending-claim, and claimed columns and checks; - composite Tenant/Project/Gateway identity and optimistic revision checks; - Tenant transaction context plus forced Row-Level Security policies; - atomic aggregate, append-only Audit, and Outbox writes; - stable evidence identities and secret-free Outbox payloads; - typed `gateway-storage-unavailable` application failures; - scripted SQL/migration behavior tests with no external service; - an opt-in PostgreSQL 18 integration test using a dedicated database and a non-superuser/non-`BYPASSRLS` application role. The SQL adapter and migration are implemented. No composition root currently opens a production database, runs migrations, exposes Gateway HTTP routes, or delivers the Outbox. Production roles, credentials, pool sizing, timeouts, backup/restore testing, managed-provider profiles, and continuously provisioned integration infrastructure remain planned. ## Implemented telemetry and CloudLink ACK slice `PostgresTelemetryRepository` implements the first crash-durable telemetry acceptance slice. One Tenant-scoped transaction writes the ingress idempotency identity, batch receipt, stream and contiguous cursor state, accepted records, required Audit event, integration Outbox event, and the exact CloudLink durable ACK outbox row. Duplicate replay returns and requeues the same stored ACK rather than inserting a second fact. The adapter uses parameterized SQL, lossless `numeric(20,0)` protocol positions, composite Tenant keys and foreign keys, forced Row-Level Security, bounded reorder and quota checks, and a lease-based ACK delivery repository. The application-owned delivery use case validates claimed adapter output, publishes sequentially, records completion only after publish succeeds, and releases failures with a sanitized code. The opt-in PostgreSQL 18 tests use a non-superuser/non-`BYPASSRLS` application role and inject failures on both sides of commit. A pre-commit failure leaves no telemetry fact and no ACK. A post-commit uncertain result leaves one committed fact, and a worker later publishes the identical ACK; replay does not create a second fact. This evidence covers accepted telemetry only. Production database and worker composition, CloudLink session/epoch persistence, data-loss facts, multi-sample mapping, and a full production crash-durable release gate remain planned. Read [Gateway identity and enrollment](/en/aethercloud/concepts/gateway-identity-and-enrollment) for the tenant-scoped identity and persistence boundary visible to gateway users. --- # Resource model > Authoritative source: [AetherCloud](https://github.com/EvanL1/AetherCloud/blob/main/docs/concepts/resource-model.md). This page is mirrored into the unified AetherIoT documentation. AetherCloud uses explicit IoT nouns instead of a universal entity table. The model can gain typed relationships later, but the foundational ownership and identity hierarchy remains visible. ```text Tenant └── Project ├── CloudConnection │ └── DeploymentStack │ └── CloudResourceProjection └── Site └── Gateway └── Instance └── Point ``` ## Core resources **Tenant** is the security, billing, retention, and data-isolation boundary. Every owned aggregate and event includes a tenant identity. **Project** groups sites and artifacts for an application or operational team. It is not a substitute for tenant authorization. **CloudConnection** references one explicitly selected provider identity and a short-lived credential source. It records configuration and discovery status, never raw long-lived credentials. **DeploymentStack** is the smallest independently planned, applied, locked, and recovered infrastructure unit. It belongs to exactly one cloud connection and uses exactly one remote State binding. Its Tenant, Project, Provider, Connection, and State key are derived rather than repeated as caller-provided values. Region placement retains its observation time for future freshness policy. The domain definition is implemented; persistence and lifecycle execution are planned. **CloudResourceProjection** is AetherCloud's normalized, time-stamped view of a provider resource. The infrastructure provider remains authoritative for the resource's actual existence and lifecycle state. **Site** is a physical or logical operating location that remains meaningful while gateways are replaced. **Gateway** is one enrolled AetherEdge runtime identity. A reinstall or hardware replacement must follow an explicit identity-recovery workflow. The implemented Gateway foundation distinguishes registration, a short-lived enrollment claim, and claim consumption. Active credential binding, CloudLink session, runtime health, revocation, and recovery remain separate planned facts. **Instance** is an edge thing-model instance reported by a gateway. Its cloud identity includes its gateway context; edge-local numeric identifiers are not globally unique. **Point** is a typed measurement, status, command, or adjustment point within an instance. The cloud stores metadata and historical projections, not the authoritative live value. ## Identity rules - Public resource identities are opaque strings, normally UUIDs. - Edge-local sequence values and identifiers preserve their original integer width in wire contracts. - Display names are mutable and never used as durable foreign keys. - Provider identities are explicit, stable, lower-case identifiers; they are never inferred from credential shape. - Portable capabilities use core names. Provider-specific capabilities use a namespaced identifier so the common model stays open-ended. - Moving a gateway between tenants is an explicit security-sensitive workflow, not an ordinary update. - Domain Packs add typed metadata and knowledge; they do not replace the core ownership hierarchy. ## State vocabulary Use `desired` for a cloud-authored target, `reported` for an edge observation, and `applied` only when the edge reports successful activation. Use `connected` for a recent authenticated session, not as proof that devices behind the gateway are healthy. See the [terminology reference](/en/aethercloud/reference/terminology) before adding new state names. See [Gateway identity and enrollment](/en/aethercloud/concepts/gateway-identity-and-enrollment) for the implemented foundation and explicit exclusions. --- # AetherIoT product family > Authoritative source: [AetherCloud](https://github.com/EvanL1/AetherCloud/blob/main/docs/get-started/aetheriot-product-family.md). This page is mirrored into the unified AetherIoT documentation. AetherIoT is the umbrella project and public platform identity. It is not an additional runtime, control plane, or protocol. ```text AetherIoT ├── AetherEdge edge runtime, Kernel, CLI, and SDK ├── AetherCloud cloud fusion and governed control plane └── AetherContracts public specifications, Schemas, fixtures, and TCK AetherEMS industry solution built on the platform ``` The edge repository formerly named `EvanL1/AetherIot` moves to `EvanL1/AetherEdge`. Existing `aether-*` crates and binaries, the `aether` CLI, `aether-edge-sdk`, configuration identifiers, installer names, and CloudLink contract identifiers remain stable. ## Authority does not move with the name - AetherEdge remains authoritative for live point state, acquisition, deterministic rules, safety interlocks, local policy, and final physical execution. - AetherCloud remains authoritative for desired placement and governed cloud jobs. - A provider remains authoritative for the actual existence and native state of its resources. - AetherContracts remains the sole shared interoperability authority. Published tags, evidence, provenance records, and digest-pinned AetherContracts releases are immutable. The alpha.3 release and imported consumer closure may retain the historical AetherIot name. Repository renaming does not change their conformance status. The common documentation entry point is [docs.aetheriot.workers.dev](https://docs.aetheriot.workers.dev/). Its primary sections are Overview, AetherEdge, AetherCloud, AetherContracts, Tutorials, Compatibility, and Roadmap. Product repositories remain authoritative for implementation details. The public [AetherIot to AetherEdge migration guide](https://docs.aetheriot.workers.dev/migration/aetheriot-to-aetheredge/) explains repository URL changes and the identifiers that remain stable. --- # AetherCloud overview > Authoritative source: [AetherCloud](https://github.com/EvanL1/AetherCloud/blob/main/docs/get-started/overview.md). This page is mirrored into the unified AetherIoT documentation. AetherCloud is the optional, AI-native multi-cloud fusion and control plane for AetherEdge. It gives people, applications, and coding agents one place to understand fleets and cloud resources, choose placements, inspect telemetry, publish versioned artifacts, and coordinate audited work across edge sites and infrastructure providers. It is not the runtime that acquires device data or closes a control loop. An AetherEdge host continues operating when AetherCloud, the wide-area network, or an AI provider is unavailable. ## What belongs in AetherCloud - Tenant identity, users, permissions, projects, and fleet inventory - Cloud connections, provider capability discovery, and normalized inventory - Placement decisions and provider-scoped deployment stacks - Audited infrastructure plan and apply jobs - Gateway enrollment and connection observations - Versioned Pack, configuration, model, and application artifacts - Separate desired, reported, and applied revision facts - Durable telemetry and alarm projections received from an edge - Expiring, audited capability jobs and their receipts - APIs and documentation for human-authored and agent-authored applications ## What remains at the edge - Device protocol sessions and acquisition - Authoritative live point values - Commissioned channel configuration - Deterministic rules, alarms, interlocks, and fallback behavior - Validation and final acceptance of work that can affect a physical system - Bounded store-and-forward behavior during disconnection ## What remains authoritative at a provider - The actual existence and lifecycle state of provider resources - Provider-native identity, quotas, regions, failure domains, and capabilities - The remote, locked infrastructure state for one deployment stack AetherCloud owns desired placement and orchestration state, but does not pretend that a normalized projection is the provider's actual state. Read the [multi-cloud fusion model](/en/aethercloud/concepts/multi-cloud-fusion) before adding a provider or infrastructure workflow. Read the [authority boundary](/en/aethercloud/concepts/edge-cloud-boundary) before adding a feature that crosses the network. ## Delivery sequence The repository begins with a TypeScript modular monolith and agent-readable contracts. Provider discovery, governed infrastructure Plan, and the Gateway registration/claim foundation have implemented domain/application slices. Memory adapters keep them self-contained; their production adapters and public Tenant routes remain planned. The infrastructure engine is deliberately Plan-only, with no Apply operation. The IoT product sequence is Gateway identity, CloudLink session, runtime manifest, telemetry/alarm ingestion, artifact registry, desired/reported/applied deployment, governed Jobs, operational integration, and MCP. Each slice must be useful without requiring the later slices. Device control is not part of the repository-foundation milestone. Read the [capability map](/en/aethercloud/concepts/iot-cloud-capability-map) for the full bounded contexts and the [vertical-slice roadmap](/en/aethercloud/guides/iot-cloud-roadmap) for phase gates and exclusions. ## Start developing Read the [repository layout](/en/aethercloud/reference/repository-layout), then follow the [agent development guide](/en/aethercloud/guides/build-with-an-agent). The current API surface is documented in the [HTTP reference](/en/aethercloud/reference/http-api). --- # Add a Provider Adapter > Authoritative source: [AetherCloud](https://github.com/EvanL1/AetherCloud/blob/main/docs/guides/add-provider-adapter.md). This page is mirrored into the unified AetherIoT documentation. This guide describes the implemented read-only provider discovery contract. It does not describe a generic cloud SDK wrapper. A Provider Adapter translates one explicit CloudConnection into provider observations while preserving provider-native identifiers and namespaced capabilities. ## Implemented contract The current foundation includes: - `CloudProviderDescriptor`, `CloudConnection`, and `ProviderRegion` domain values - the application-owned `ProviderAdapter` port and dynamic `ProviderAdapterRegistry` - the tenant/project-scoped `DiscoverProviderRegions` query with the `cloud-connections:read` permission - typed authentication, configuration, permission, rate-limit, and availability failures - application-boundary checks for provider identity, connection identity, observation time, duplicate regions, and undeclared capabilities - `providerAdapterConformance`, a reusable adapter test suite - `MemoryProviderAdapter`, a deterministic test adapter No provider SDK, credential resolver, persistence adapter, public discovery endpoint, or infrastructure mutation is implemented yet. ## Adapter workflow 1. Define one descriptor with a stable lower-case provider identity, provider kind, portable capabilities, and namespaced provider capabilities. 2. Implement the application-owned `ProviderAdapter` interface outside `packages/domain` and `packages/application`. 3. Accept only a CloudConnection resolved for the authorized tenant and project scope. Its `credentialSource` is a workload identity or secret reference, never the credential value. 4. Resolve short-lived access inside the adapter boundary. Do not place tokens in a snapshot, error, log, fixture, prompt, or generated module. 5. Map expected provider failures to the typed result. Do not leak an SDK error as an application contract or report rate limiting as an empty region list. 6. Return canonical UTC observation time, the exact Provider and Connection identities, unique regions, and capabilities declared by the descriptor. 7. Run the shared conformance suite and provider-specific fixtures. 8. Register the adapter only in a composition root. Minimal shape: ```ts export class ExampleProviderAdapter implements ProviderAdapter { readonly descriptor = exampleProviderDescriptor; async discoverRegions( request: ProviderRegionDiscoveryRequest, ): Promise { // Resolve short-lived access from request.connection.credentialSource. // Decode the provider response and return typed domain observations. } } ``` ## Required conformance Every adapter test imports `providerAdapterConformance` from `@aether-cloud/provider-conformance`. Supply a fresh adapter and a CloudConnection belonging to that provider. The shared suite checks the provider and connection scope, canonical observation time, unique region identities, declared capabilities, and rejection of a mismatched connection. Provider-specific tests remain necessary for response decoding, pagination, authentication, throttling, quota behavior, and malformed upstream data. The default test path uses fixtures and must not call a real cloud account. ## Separate lifecycle support Infrastructure planning is now implemented behind a separate plan-only `InfrastructureEngine` port. Real local OpenTofu Plan process execution is implemented; Terraform process execution and infrastructure mutation remain planned. A future provider adapter may select versioned provider modules and normalize provider observations, but do not add `plan`, `apply`, `destroy`, import, or State repair to the read-only discovery method. Read [multi-cloud fusion](/en/aethercloud/concepts/multi-cloud-fusion) for State and authority rules and [repository layout](/en/aethercloud/reference/repository-layout) for dependency placement. --- # Build with an AI agent > Authoritative source: [AetherCloud](https://github.com/EvanL1/AetherCloud/blob/main/docs/guides/build-with-an-agent.md). This page is mirrored into the unified AetherIoT documentation. AetherCloud documents architecture, product semantics, and implemented surfaces as versioned repository assets so a coding agent can make a narrow change without reconstructing intent from framework code. ## Give an agent context Point the agent at the repository root and ask it to read, in order: 1. `AGENTS.md` for mandatory boundaries and verification 2. `skills/aether-cloud/SKILL.md` for task routing 3. the page selected from `llms.txt` 4. the nearest package README or contract for implementation details Provider work also reads [Add a Provider Adapter](/en/aethercloud/guides/add-provider-adapter) before changing the application port or creating SDK code. Example prompt: ```text Read AGENTS.md and the aether-cloud skill. Add a read-only provider capability query using the existing application boundary. Keep provider SDK types outside the application package, update the HTTP reference, and run pnpm check. ``` ## Require evidence An agent should identify: - the source of truth it is changing - the application use case and permission boundary - whether a contract is implemented or planned - whether intent is provider-neutral or requires a namespaced provider feature - the provider, deployment stack, State, and credential boundaries for infrastructure work - the narrow behavior test that fails before implementation - the verification commands that passed afterward Reject generated code that writes an adapter from a route handler, treats cloud telemetry as live authority, omits tenant context, or invents a CloudLink message that is not present in the versioned contract package. Also reject fixed vendor switches in core packages, provider inference from credential contents, cross-provider State, parsing human IaC output, or an infrastructure apply that lacks saved-plan and approval evidence. ## Documentation contract Every indexed page has frontmatter containing `title`, `description`, `updated`, and `status`. The same title, description, and status appear in `ai/docs-manifest.json`. Status is one of `implemented`, `planned`, `mixed`, `normative`, or `deprecated`. The repository test checks the manifest, local links, required entry points, and unfinished placeholders. When behavior changes, update code, tests, the narrow reference page, and the manifest description if its routing meaning changed. Architectural authority or dependency changes also require an ADR. ## Current verification ```bash pnpm check ``` This runs documentation contracts, unit and integration tests, type checking, linting, and formatting checks without external services. ## Agent capabilities The repository-foundation milestone provides documentation for coding agents. Remote MCP resources and operational tools are planned later, after identity, tenant authorization, and the underlying query use cases exist. Repository documentation must not instruct an agent to call those tools before they ship. --- # IoT Cloud vertical-slice roadmap > Authoritative source: [AetherCloud](https://github.com/EvanL1/AetherCloud/blob/main/docs/guides/iot-cloud-roadmap.md). This page is mirrored into the unified AetherIoT documentation. Each phase must be runnable and valuable without pretending that later phases exist. The default verification path remains independent of PostgreSQL, cloud accounts, and real AetherEdge devices through ports, memory adapters, and contract fixtures. ## Phase 0: Audit and architecture contract **Observable value.** Developers and agents can distinguish executable layers from reviewed contracts, find every authority boundary, and select the next slice without treating multi-cloud infrastructure work as the IoT product. **Model.** Bounded contexts, authority matrix, capability map, implementation status, state machines, storage ownership, and composition-root dependency rules. **API and protocol.** Machine-readable capability, permission, error, event, and HTTP-operation catalog. No new runtime endpoint or CloudLink wire message. **Persistence.** PostgreSQL, object, analytics, inbox/outbox, audit, and OpenTelemetry responsibilities are assigned without installing a service. **Security.** Edge authority, Tenant isolation, deny-by-default commands, sensitive-data exclusion, and implemented/planned truth are documentation contracts. **Tests.** Manifest/frontmatter parity, links, required assets, catalog schema, command metadata, and telemetry/observability separation. **Done when.** The evidence-backed audit, capability map, state machines, ADRs, roadmap, and Agent indexes agree and documentation contract tests pass. **Not included.** Runtime code, provider expansion, OpenTofu Apply, production infrastructure, or a fabricated edge protocol. ## Phase 1: Gateway identity and enrollment foundation **Observable value.** An authorized operator can register a Gateway in one Tenant and Project, issue one short-lived claim, let a runtime claim it, and query the resulting state through application use cases. **Model.** `TenantId`, `ProjectId`, `GatewayId`, `EnrollmentClaimId`, `EnrollmentRequestId`, `GatewayIdentity`, and the `registered -> awaiting-claim -> claimed` state machine. **API and protocol.** Implemented transport-neutral register, issue, claim, and get capabilities. No public HTTP route or CloudLink message yet. **Persistence.** Implemented optimistic repository port, memory repository, memory token service, and PostgreSQL Gateway repository/migration. The SQL adapter provides Tenant RLS and atomic aggregate/Audit/Outbox writes. Production database composition, roles, migration execution, backup/restore, and KMS token service remain planned. **Security.** Tenant-scoped permission checks, explicit confirmation for issue, server expiry, idempotency conflicts, token digest at rest, raw token returned once, generic claim rejection, and audit-required metadata. **Tests.** ID decoding, state transitions, expiry boundary, identical replay, conflicting claim, deny-by-default permission, confirmation, cross-tenant query, optimistic replacement, token verification, and full memory flow. **Done when.** Narrow tests, `pnpm check`, coverage thresholds, and production dependency audit pass; agent documentation states the exact implemented surface. **Not included.** CA/KMS, active credential, revocation, recovery, attestation, production database deployment, HTTP enrollment, CloudLink, or device control. ## Phase 2: CloudLink heartbeat and session **Current status.** The transport-neutral domain/application/memory foundation implements credential verification through a port, protocol selection, lossless epochs/cursors, fencing, resume, heartbeat, and session query. An experimental JSON/MQTT codec, broker adapter, application bridge, ingress lifecycle, Schemas, fixtures, and real-broker harness are also implemented. Production credential lifecycle, shared AetherEdge conformance, timeout scheduler, PostgreSQL session/epoch state, durable data-loss facts, multi-instance ownership, and backpressure remain planned, so Phase 2 is not complete. The accepted-telemetry PostgreSQL inbox/cursor/receipt/ACK transaction and delivery use case are implemented, but they do not complete session durability. The production release sequence is: shared-Broker origin authentication, one joint wire profile, identical fixtures, a dual Edge/Cloud Broker harness, fault injection, crash-durable ACK persistence, then explicit legacy cutover. The target profile is not yet the implemented codec. **Observable value.** An operator sees authenticated connection freshness, and a Gateway reconnects without replaying already durable envelopes. **Model.** `CredentialBinding`, `SessionEpoch`, `ProtocolVersion`, `StreamCursor`, `HeartbeatObservation`, `IngressInbox`, and flow-control credit. **API and protocol.** The provisional experimental core binding implements session hello/accepted, heartbeat/ACK, Runtime Manifest, Point telemetry, durable ACK, replay request, and data-loss wire shapes. Alpha.3 ACKs are unsigned. Production challenge/per-uplink authentication remains blocked; a future Cloud-signed ACK requires a new protocol version. Durable data-loss persistence, multi-sample Cloud indexing, and credit messages require later application work. Read-only session queries already use application use cases. **Persistence.** The accepted-telemetry PostgreSQL inbox, cursor, receipt, Audit, integration Outbox, and exact ACK outbox are implemented. PostgreSQL session, epoch, data-loss, and multi-instance ownership remain planned; socket state remains local to an independent CloudLink composition root. **Operational observability.** A neutral failure-isolated ingress observer is implemented. Wire it to the existing no-op/OpenTelemetry runtime with optional W3C Trace Context, active-session and handshake metrics, and bounded export. No Collector is needed by default. **Security.** Credential verifier port, revocation check, new-session fencing, protocol allow-list, bounded message/window limits, canonical uint64 strings, authenticated per-Gateway Broker ACLs, and optional verified out-of-band principal metadata from reviewed connector/Broker adapters. The current hello proof is structural only. Generic shared-Broker mode requires a jointly frozen establishment signature and a session-bound Gateway signature on every uplink; trusted per-publish Broker attestation is the reviewed alternative. Topic or payload identity never supplies authorization. Exact signing projections, key rotation, and revocation remain gates. **Tests.** Both repositories first close every pending AetherContracts import and execute the complete identical valid/invalid fixture set. One real Broker then runs the Edge harness and Cloud ingress while injecting disconnect, ACK loss, Edge/Cloud process restart, duplicate delivery, conflicting digest, and data loss. Unit coverage additionally includes gap, reorder window, old epoch, simultaneous connections, backpressure, heartbeat timeout, missing trace context, forbidden baggage authorization, redaction, and exporter loss. **Done when.** The independent CloudLink root and AetherEdge harness resume from the PostgreSQL durable cursor, a pre-commit crash emits no ACK, a post-commit crash republishes one stable identical ACK, and every ordered interoperability gate passes. The external-service-free default suite remains available separately. **Not included.** Artifact delivery, arbitrary RPC, physical commands, direct SHM writes, or device-register writes. Legacy remains the default until the phase is complete. ## Phase 3: Runtime manifest and capability catalog **Current status.** The bounded AetherEdge Runtime Manifest v1 report/query slice is implemented in domain, application, Node checksum, and memory repository layers. It validates exact external fields, verifies the AetherEdge canonical JSON SHA-256 vector, derives scope from an active credential, preserves lossless generations and late history, rejects conflicting reuse, and exposes the current capability catalog. PostgreSQL, durable audit/outbox, CloudLink wire transport, public fleet API, freshness policy, and Instance/Point catalog remain planned, so Phase 3 is not complete. **Observable value.** Operators and deployment planners can query runtime versions, reported Instances/Points metadata, and supported edge capabilities with observation time and freshness. **Model.** Implemented `RuntimeManifestObservation`, `ManifestGeneration`, and capability/protocol catalog values; planned `RuntimeVersion`, `InstanceDescriptor`, `PointDescriptor`, `CapabilityDescriptor`, `ManifestGeneration`, and freshness. **API and protocol.** Implemented transport-neutral report command and current manifest query. A versioned manifest observation envelope, public fleet/version queries, and dynamic groups with typed predicates remain planned. **Persistence.** Implemented atomic memory latest/history and request replay adapter for conformance. PostgreSQL latest/historical metadata is planned; object storage is considered only if bounded relational storage is insufficient. **Security.** Authenticated Gateway binding, schema/version validation, count and size quotas, and no cloud mutation of commissioned edge channels. **Tests.** Duplicate and old generation, reorder, unknown capability, unsupported version, excessive size, stale projection, and tenant isolation. **Done when.** Fleet queries return capability provenance, `observedAt`, and freshness; deployment compatibility can consume the application contract. **Not included.** Artifact publication, rollout, Job execution, or a universal Entity/Relation/Attribute store. ## Phase 4: IoT telemetry ingestion **Current status.** The protocol-neutral domain/application slice, canonical digest, memory inbox/history/outbox/audit adapter, durable receipt, duplicate and conflicting replay handling, gap/coalescing behavior, quota, scoped history query, OpenTelemetry ingestion decorator, edge-native Point identity, topology binding, and experimental CloudLink MQTT mapping are implemented. PostgreSQL, accepted-fact/history/cursor storage, Audit/integration Outbox, exact ACK outbox, and a bounded delivery use case are also implemented and PostgreSQL 18 verified. Public HTTP, production database/worker composition, durable data-loss, multi-sample mapping, downsampling, and export remain planned, so the production phase is partial. **Observable value.** Tenants query replay-safe Point and device-event history without confusing it with authoritative edge live state. **Model.** `TelemetryStream`, `TelemetryBatch`, `PointSample`, `DeviceEvent`, `StreamEpoch`, lossless `StreamPosition`, `IngestionReceipt`, `RetentionClass`, and gap marker. **API and protocol.** Protocol-neutral ingest and history queries plus an experimental versioned MQTT envelope are implemented. The envelope remains a pre-release candidate. Durable ACK follows application acceptance; the bridge can use the exact PostgreSQL-persisted projection, while authentication and production composition remain gated. **Persistence.** PostgreSQL inbox, cursor, metadata, quota, initial bounded history, Audit, integration Outbox, and exact ACK outbox are implemented; raw/cold batches may move to object storage when justified through a replaceable history-store port. **Security.** Credential-derived Tenant/Gateway scope, decompression and batch limits, canonical int64 encoding, quota, retention, payload decoding, and explicit freshness. OpenTelemetry labels exclude resource identities. **Tests.** Valid/invalid batch, revoked Gateway, replay, same-ID conflict, gap, reorder, atomic batch failure, int64 boundaries, crash before/after commit, disconnect, retention overflow, exporter failure isolation, and history query. **Done when.** Ack follows durable acceptance; replay produces no duplicate fact or event; history exposes source/ingest time, position, quality, and known gaps; no code presents it as live authority. **Not included.** Cloud live-state authority, cloud deterministic rules, high-cardinality Point metrics, or a claim that the experimental schema is a released cross-repository contract. ## Phase 5: Alarm projection **Current status.** Edge alarm-fact domain projection, authenticated ingestion, Tenant query, cloud-only acknowledgement, canonical digest, replay/conflict/gap/ late behavior, and a memory projection/audit/outbox adapter are implemented. PostgreSQL, CloudLink wire, public HTTP/search, assignment, comments, and freshness reconciliation remain planned, so the production phase is partial. **Observable value.** Tenants search replay-safe edge alarm facts and manage cloud-only acknowledgement, assignment, and comments without changing edge safety state. **Model.** `AlarmOccurrence`, `AlarmFact`, `AlarmProjection`, edge generation and sequence, workflow acknowledgement, assignment, comment, and freshness. **API and protocol.** Authenticated alarm-fact ingestion, alarm search/timeline, and cloud workflow commands through shared application use cases. **Persistence.** PostgreSQL inbox, append-only facts, projection, workflow, audit, and outbox. **Security.** Gateway binding, bounded payload, Tenant isolation, cloud workflow permissions, and no cloud operation that clears a local alarm or interlock. **Tests.** Duplicate/conflicting fact, clear-before-raise, missing predecessor, late update, disconnect stale state, re-raise generation, cross-Tenant query, workflow audit, and projection retry. **Done when.** Every projection exposes completeness and freshness, edge fact state remains separate from cloud workflow, and replay is idempotent. **Not included.** Cloud-authoritative deterministic alarms, physical interlock mutation, or alarm clearing through CloudLink. ## Phase 6: Artifact registry **Current status.** The explicit Artifact/Revision domain lifecycle, lossless revision/content-length values, immutable publication, compatibility and dependency metadata, digest/content verification port, signature verifier port, release-channel conflict protection, publish/query use cases, and atomic memory metadata/content/audit/outbox adapter are implemented. PostgreSQL, production object storage, KMS-backed signature verification, upload HTTP, deprecate/withdraw commands, and edge download remain planned, so the production phase is partial. **Observable value.** Teams publish immutable, verifiable Pack, configuration, model, rule, and application revisions with provenance and compatibility. **Model.** `Artifact`, `ArtifactRevision`, `ContentDigest`, `Signature`, `CompatibilitySpec`, `ReleaseChannel`, and `Publication`. **API and protocol.** Upload/finalize, validate, publish, deprecate, withdraw, channel update, and read/query APIs. Edge download protocol remains reference by digest rather than inline mutable content. **Persistence.** PostgreSQL metadata; object storage blob, signature, SBOM, and provenance; transactional outbox for publication events. **Security.** Digest verification, immutable writes, signer/provenance policy, malware/content validation hooks, upload limits, and permissioned publication. **Tests.** Tampered upload, duplicate digest, validation failure, signature failure, publish race, conflicting idempotency, channel race, and withdrawal. **Done when.** A published revision is immutable, independently verifiable, and stable enough for desired-state references. **Not included.** Rollout scheduling or edge activation. The implemented layer contract and remaining production boundary are detailed in [Artifact registry and immutable publication](/en/aethercloud/concepts/artifact-registry). ## Phase 7: Desired, reported, and applied deployment **Current status.** A single-Gateway domain/application/memory foundation is implemented. It verifies a published Artifact, creates lossless Desired generations, keeps Reported and Applied separate, authenticates edge observations, represents timeout as unknown without an Applied fact, preserves late observations, supports pause/resume/cancel-request and rollback as a new generation, and atomically records memory audit/outbox evidence. PostgreSQL, durable audit/outbox, target snapshots, canary/batch scheduling, CloudLink wire, HTTP, and AetherEdge delivery remain planned, so the production phase is partial. **Observable value.** Operators perform canary/batched rollout, pause/resume, cancel remaining targets, roll back through a new generation, and see truthful divergence and partial success. **Model.** `Deployment`, `Rollout`, `TargetSnapshot`, `DesiredGeneration`, `ReportedRevision`, `AppliedEvidence`, rollout policy, and target result. **API and protocol.** Start/pause/resume/cancel-request/rollback commands, divergence queries, immutable revision offer, and edge accept/reject/fetch/ validate/apply observations. **Persistence.** PostgreSQL aggregate, target state, inbox/outbox, and audit; object storage for large evidence. **Security.** Artifact compatibility, target snapshot, permission/risk/ confirmation, expiry, precondition, and anti-TOCTOU policy checks. **Tests.** Old report, duplicate desired generation, partial success, disconnect unknown, cancel race, paused delivery, late applied evidence, rollback, and target replacement during rollout. **Done when.** Desired, reported, and applied are separately queryable and no intermediate delivery state is presented as applied. **Not included.** General-purpose edge capability commands. See [Desired, Reported, and Applied deployment](/en/aethercloud/concepts/desired-reported-applied-deployment) for the implemented boundary. ## Phase 8: Governed capability Jobs **Current status.** The capability-gated domain/application/memory foundation is implemented. It derives governance from declarations, requires platform and capability permissions, supports confirmation/queue/offer/unknown/cancel, orders lossless edge Receipts across gaps, binds ingestion to an active Gateway credential, and atomically records memory audit/outbox evidence. PostgreSQL, Runtime Manifest declaration provenance, CloudLink delivery, workers, public HTTP, evidence storage, and the AetherEdge counterpart remain planned, so the production phase is partial. **Observable value.** Tenants first run low-risk diagnostics and later approved commands through one auditable Job lifecycle with edge rejection preserved. **Model.** `CapabilityDeclaration`, `CapabilityJob`, `Risk`, `Confirmation`, `Precondition`, `ExecutionAttempt`, `Receipt`, and evidence reference. **API and protocol.** Create, confirm, offer, cancel-request, status, and Receipt ingestion. Capability schemas come from the runtime catalog; no arbitrary RPC. **Persistence.** PostgreSQL Job ledger, immutable Receipt inbox, audit, and outbox; object storage for large evidence. **Security.** Permission, risk, confirmation, idempotency, expiry, precondition, audit, edge-side acceptance, and explicit non-idempotent unknown semantics. **Tests.** Missing governance metadata, permission/confirmation failure, precondition drift, duplicate, conflicting duplicate, edge rejection, timeout, late Receipt, retry, cancel race, partial result, and network partition. **Done when.** Creation through final Receipt has a correlated immutable audit chain and no transport bypass exists. **Not included.** Physical control enabled by default; such capabilities remain deny by default and require additional policy evidence. See [Governed capability Jobs](/en/aethercloud/concepts/governed-capability-jobs) for the implemented boundary. ## Phase 9: Audit, query, integration, operations, and observability **Current status.** Tenant/Project-scoped audit values and cursor query, an authenticated JSON route, a finite `Last-Event-ID` resumable SSE snapshot, webhook subscription and bounded delivery/retry/dead-letter/redrive state machines, asynchronous Data Export request/outcome/query, and memory adapters are implemented. PostgreSQL durability, production identity, live notification, destination registry/secrets/signing/SSRF defence, workers, object storage, download, quota, fleet health, and WebSocket remain planned, so Phase 9 is partial. **Observable value.** Operators search the complete action history, monitor connection/data/deployment/Job health, and safely subscribe, export, or deliver webhooks. **Model.** `AuditEvent`, `FleetHealthProjection`, `Subscription`, `WebhookEndpoint`, `DeliveryAttempt`, `DeadLetter`, `ExportJob`, `Quota`, and `RetentionBudget`. **API and protocol.** Tenant-filtered audit/fleet queries, SSE/WebSocket event resume, webhook/subscription/export commands, and dead-letter redrive. **Persistence.** PostgreSQL audit/outbox/delivery and summary state; object storage exports; optional OpenTelemetry traces/metrics backend; broker only after measured need. **Security.** Append-only audit, redaction, webhook SSRF prevention and signing, secret rotation, Tenant-filtered streams, quotas, and rate limits. **Tests.** Cross-tenant search, duplicate delivery, exponential retry, dead letter, redrive, stream disconnect/resume, webhook rotation, quota, retention, projection freshness, trace propagation, redaction, metric cardinality, bounded export, and exporter failure isolation. **Done when.** Every earlier command and ingestion path is correlated in audit, and health views expose source time and freshness. **Not included.** Kafka, Redis, Kubernetes, or service extraction without measured operational evidence. ## Phase 10: MCP resources and tools **Current status.** A transport-neutral MCP interface lists capability and Audit resources, reads Tenant audit evidence through `SearchAuditEvents`, advertises complete governance metadata, and exposes `data.export.request` and `edge.job.create` through their existing application commands. Planned tools fail closed. An MCP SDK transport, stdio/Streamable HTTP composition root, production identity/audit, rate limit, and remaining resources/tools are planned, so Phase 10 is partial and no MCP client can connect to a wire server yet. **Observable value.** Coding and operations agents read real product contracts, capability metadata, and Tenant projections, then invoke only existing governed use cases. **Model.** `DocumentationResource`, `CapabilityDefinition`, tool input/output schema, actor provenance, and audit correlation. **API and protocol.** Read-only documentation and query resources first. A command tool maps one-to-one to an implemented application command and displays permission, risk, confirmation, idempotency, and expiry before invocation. **Persistence.** Reuse normal application ports and audit; no agent-only data path. **Security.** Tenant authorization, least privilege, explicit confirmation, prompt/output redaction, provenance, rate limit, and deny-by-default discovery. **Tests.** HTTP/CLI/MCP use-case parity, planned capability rejection, metadata completeness, cross-tenant denial, confirmation, secret redaction, and audit. **Done when.** An agent can determine implemented/planned status and governance requirements before a call, and every tool result traces to the same use case. **Not included.** A chatbot that writes storage, an agent-only CloudLink path, or direct physical control. --- # Plan infrastructure safely > Authoritative source: [AetherCloud](https://github.com/EvanL1/AetherCloud/blob/main/docs/guides/plan-infrastructure.md). This page is mirrored into the unified AetherIoT documentation. This guide describes AetherCloud's implemented infrastructure Plan boundary. It produces an immutable, policy-evaluated receipt for one Deployment Stack. There is no Apply operation in the current `InfrastructureEngine` port, so a successful or policy-approved Plan cannot mutate provider infrastructure. ## Implemented contract `PlanDeploymentStack` is the transport-neutral application command. It: 1. runtime-decodes Tenant, Project, Stack, engine, idempotency, expiry, module, topology, and content digests; 2. requires the `infrastructure.stack.plan` permission before resolving a Stack or invoking an engine; 3. replays the same scoped idempotency request and rejects conflicting reuse; 4. resolves exactly one `opentofu` or `terraform` implementation from the dynamic `InfrastructureEngineRegistry`; 5. supplies one generated Plan identity, one Deployment Stack, and immutable module and topology selections to `InfrastructureEngine.plan`; 6. fails closed unless the result targets the exact Stack and State, proves that the State lock was acquired and released, and references an encrypted sensitive Plan artifact by digest; 7. summarizes create, update, delete, replace, and read changes for storage and policy without returning provider resource details in the Plan receipt; 8. records the policy decision as `policy-approved` or `policy-rejected` and keeps approval at `not-requested`. The Plan repository key is Tenant + Project + idempotency request. A request in one tenant cannot retrieve another tenant's receipt. ```text authorized Plan request ↓ tenant/project Stack lookup → one remote State key ↓ InfrastructureEngine.plan → encrypted saved-Plan reference + lock evidence ↓ runtime contract checks → change summary → policy ↓ idempotent Plan receipt (never Apply) ``` ## Sensitive Plan data Treat the saved Plan binary and raw versioned JSON as secret-bearing data. Both [OpenTofu `show`](https://opentofu.org/docs/v1.10/cli/commands/show/) and [Terraform `show`](https://developer.hashicorp.com/terraform/cli/commands/show) warn that sensitive values can appear in plaintext JSON output. Raw Plan JSON is sensitive: never put it in logs, audit payloads, HTTP responses, test snapshots, prompts, or agent context. The application receives only the validated change list needed for policy, then persists a summary plus the encrypted artifact reference and SHA-256 digest. The actual CLI worker must write the saved Plan and JSON into an isolated workspace, encrypt the durable artifact, and destroy its workspace after upload. It must never commit either file to source control. ## Implemented OpenTofu worker `OpenTofuInfrastructureEngine` under `adapters/infrastructure/opentofu` is a real Plan-only CLI adapter. Its async factory executes `tofu version -json` and derives the descriptor version from that response instead of hard-coding one. For each Plan it uses `NodeOpenTofuProcessRunner` to pass an executable and argv directly to `child_process.spawn` with `shell: false`: ```text tofu init -input=false -no-color tofu validate -json tofu plan -json -input=false -lock=true -lock-timeout=… \ -detailed-exitcode -no-color -out= tofu show -json ``` Plan exit code 0 means a successful empty diff, 2 means a successful non-empty diff, and every other exit is a typed failure. The versioned `show` JSON maps create, update, delete, read, no-op, and both replacement action orders into the application contract. Unsupported JSON major versions and unknown actions fail closed. Module configuration and topology variables enter through an injected artifact resolver. The worker verifies each selected SHA-256 digest before writing JSON to `main.tf.json` and `aether.auto.tfvars.json`. An injected lock manager must return a real lease for the exact Stack State key, and the result is successful only after that lease reports successful release. OpenTofu also always receives `-lock=true`. The Node workspace factory creates a fresh 0700 directory and writes source and saved Plan files as 0600. It rejects symbolic-link and oversized saved Plans, bounds captured subprocess output, handles deadlines and cancellation, and removes the complete workspace on success or failure. The subprocess receives only the explicitly supplied environment; observer events contain only stage, outcome, exit status, duration, and correlation identity. ## Adapter conformance and tests Every infrastructure engine adapter must run `infrastructureEngineConformance` from `@aether-cloud/infrastructure-conformance`. The shared suite verifies Plan, Stack, and State correlation; encrypted sensitive-artifact metadata; lock evidence; JSON format version; and the absence of `apply`. `MemoryInfrastructureEngine`, `InMemoryInfrastructurePlanRepository`, and the fake boundaries around the OpenTofu adapter keep the default test path free of an OpenTofu binary, network, remote State, secret provider, and cloud account. The real provider-free local CLI integration is explicitly opt-in: ```bash pnpm test:opentofu-integration ``` It uses the installed OpenTofu binary, an ephemeral local workspace, an in-memory lock lease, and an AES-GCM test store. It proves the real command sequence and cleanup without claiming production State or storage. ## Still planned Production remote State integration, a distributed lock implementation, short-lived credential resolution, a production encrypted artifact store, durable Plan repository, audit sink, cost model, sandboxed worker deployment, Terraform CLI adapter, and public HTTP endpoint remain planned. The implemented Node adapter is a real local OpenTofu process, but it is not proof of a real cloud account or production remote State. Apply, refresh mutation, import, and destroy remain completely unimplemented future commands with their own permission, confirmation, approval, audit, and recovery contracts. Do not add them as modes or flags on `PlanDeploymentStack`. Read [multi-cloud fusion](/en/aethercloud/concepts/multi-cloud-fusion) for provider and State boundaries and [AI invariants](https://github.com/EvanL1/AetherCloud/blob/main/ai/invariants.md) before extending this workflow. --- # AetherCloud AetherCloud is the AI-native, industry-neutral, multi-cloud IoT fusion and control plane for AetherEdge runtimes and cloud-side workloads. It is a separate product, not a hosted copy of the edge runtime. ## Implemented foundations - Provider-neutral discovery and governed infrastructure planning through capability-driven adapters. - A Plan-only OpenTofu infrastructure engine with lock and process safety evidence. - Gateway identity and enrollment, CloudLink session and runtime-manifest application foundations. - Partial PostgreSQL persistence for gateway and accepted telemetry facts, including durable acknowledgement outbox evidence. - Partial artifact, deployment, governed job, audit, integration, observability, and transport-neutral MCP application slices. ## Still planned for production Production identity and credential lifecycle, public CloudLink composition, complete crash durability, multi-sample mapping, production database composition, workers, hardened outbound delivery, and a connectable MCP server remain planned or gated. AetherCloud owns desired placement. A provider owns its actual resources. AetherEdge remains authoritative for live point state and final physical execution. Read the [AetherCloud repository](https://github.com/EvanL1/AetherCloud), the [platform overview](/en/overview/platform), and the [status page](/en/roadmap/status). --- # Application contract catalog > Authoritative source: [AetherCloud](https://github.com/EvanL1/AetherCloud/blob/main/docs/reference/application-contracts.md). This page is mirrored into the unified AetherIoT documentation. [`ai/application-contracts.json`](https://github.com/EvanL1/AetherCloud/blob/main/ai/application-contracts.json) is the machine-readable index for application capabilities, permissions, command governance, typed errors, integration events, HTTP operations, and implementation status. `mcp_exposures` separately reports whether a resource/tool adapter is implemented and whether a connectable protocol transport exists. This prevents an executable transport-neutral interface from being described as an MCP server before a composition root is available. The catalog is an index, not an alternate execution path. TypeScript domain and application code remains authoritative for executable behavior, and transport schemas remain authoritative for their encoded inputs and outputs. ## Status meanings - `implemented` means the named transport and its production-required behavior are executable. - `partial` identifies exact executable layers while listing missing production or transport layers. - `planned` is a reviewed name or contract with no executable product surface. - `deprecated` remains listed only for migration. An application use case backed only by a memory adapter is `partial`, even when its domain and application tests pass. An event is `planned` until a business transaction writes it to a real outbox. A documented CloudLink message is not implemented until both wire decoding and the owning application behavior are executable. ## Command governance Every command entry declares permission, risk, confirmation, idempotency, expiry, and audit. Missing metadata means the command cannot be exposed. A transport may add stricter policy but cannot weaken catalog governance. Queries declare a permission and never mutate product state. Edge-originated observations that change a projection are commands even when they describe a fact rather than request physical work. ## Compatibility `schema_version` changes before incompatible catalog structure changes. Capability names, error codes, event names, and operation identifiers remain stable within a version. Adding a field is backward compatible only when old readers may safely ignore it. The catalog currently documents two implemented public HTTP operations and two authenticated Audit query operations (JSON and finite SSE snapshot); partial provider, Plan, enrollment, CloudLink session, Runtime Manifest, telemetry, alarm, Artifact Registry, deployment, governed Job, audit, webhook, and Data Export application slices; and planned later IoT product capabilities. Gateway registration and enrollment events are now `partial` because the PostgreSQL adapter writes them into the same transaction as aggregate and Audit state. The catalog now names the experimental CloudLink MQTT layer separately from production composition and joint AetherEdge conformance. It does not claim a production live event stream, external webhook sender, export worker/download interface, MCP wire server, production CloudLink process, migration runner, or deployed production database exists. --- # Pre-release CloudLink MQTT v1 > Authoritative source: [AetherCloud](https://github.com/EvanL1/AetherCloud/blob/main/docs/reference/cloudlink-mqtt-v1.md). This page is mirrored into the unified AetherIoT documentation. This is an experimental, pre-release interoperability candidate. The public AetherContracts `v0.1.0-alpha.3` release is the sole contract authority, and AetherCloud and AetherEdge commit the same digest-pinned consumer lock. The current claim is complete distribution adoption and public fixture execution, not production protocol conformance. Shared-Broker production key lifecycle is still unresolved, and alpha.3 durable ACKs are explicitly unsigned. ## Public release consumption The offline consumer check pins the annotated tag object, commit, bundle size and SHA-256, release manifest SHA-256, and each adopted destination byte. It imports the exact alpha.3 adoption closure, including the normative spec, profiles, gates, failure taxonomy, TCK manifest, Schemas, fixtures, and verifier closure. `pending_imports` is empty. The local `contract-manifest.json`, `wire-profile.json`, authentication Schema, gate file, and scenarios are integration history or AetherCloud proposals. They do not override AetherContracts. Distribution integrity alone is not codec or behavioral conformance. The complete public fixtures and opt-in dual harness now provide alpha evidence, while authentication and production durable-store gates remain open. The default check is offline; the opt-in CI check downloads only the exact digest-pinned release. ## Implemented Cloud surface - `adapters/cloudlink/mqtt` strictly decodes the pinned public snake_case session, heartbeat, Runtime Manifest, telemetry, and data-loss uplinks and encodes session, heartbeat/durable ACK, and replay downlinks. - The codec validates topic/body Gateway binding, UUID identity, canonical integers/times, JCS business digests, Runtime Manifest checksum, Point kinds, topology evidence, limits, and unknown fields. - MQTT.js accepts configurable endpoint, Broker credentials, strict CA/client certificate/private-key file mTLS, MQTT 3.1.1 or MQTT 5, QoS 1, and `retain=false`; correctness uses only the 3.1.1 baseline. TLS files must be bounded regular non-symlink files, and a private key must deny group/other access. - `apps/cloudlink` routes through existing application use cases and produces no ACK without an application receipt. For telemetry, the bridge can consume the exact ACK projection committed by `PostgresTelemetryRepository`; the current Broker harness roots still use memory and are conformance evidence, not production durability. - The current Cloud telemetry model safely maps only a one-sample wire batch because it indexes records while alpha.3 positions an atomic batch. Larger batches fail explicitly until Cloud freezes that internal mapping. Data-loss persistence is also planned; the alpha harness records an application fact but does not claim production durability. - Default tests need no Broker, Docker, PostgreSQL, or cloud account. The opt-in alpha harness starts real Mosquitto, AetherEdge's rumqttc transport and file spool, plus AetherCloud's MQTT ingress and application use cases. ## Provisional wire boundary The normative public core is in the locked AetherContracts release. The local `contracts/cloudlink/v1/wire-profile.json` records an earlier Cloud proposal and migration differences; it is not shared authority: - closed snake_case UTF-8 JSON, 256 KiB maximum; - MQTT 3.1.1, QoS 1, non-retained messages; - canonical uint64 strings and Unix epoch milliseconds; - lowercase UUID Gateway/session identity; - one stream position per atomic batch, at most 256 Point samples; - RFC 8785 JCS SHA-256 over `{protocol_version,message_kind,payload}` only; - stable epoch, position, batch ID, and digest across replay; - durable ACK bound to session, stream, epoch, position, batch, digest, and Cloud receipt; - explicit replay request and data-loss evidence; - no RPC, physical-control topic, SHM write, Point/Register write, or fabricated Thing Model revision. Telemetry retains Edge-owned `instance_id`, `point_kind`, `point_id`, value, source timestamp, quality, and topology evidence. `model` is optional and may appear only when commissioning established it. ## Session and shared-Broker boundary The alpha.3 hello declares either `gateway-signed` or `trusted-connector-broker-attestation` origin. Gateway-signed hello carries the exact challenge ID, Gateway key ID, and structurally validated Ed25519 signature object. This proves the frozen codec shape, not production message origin or production key lifecycle. One successful hello is insufficient when a different publisher can later inject into a shared namespace. Generic customer-selected Broker mode therefore requires a replay-bounded Cloud challenge and Gateway signature, followed by a session-bound Gateway signature on every uplink. Cloud challenges use the experimental Cloud signature transcript; alpha.3 durable ACKs are unsigned. The alternative is trusted Broker attestation supplied by a trusted adapter outside the payload for every publish. Payload-supplied attestation is untrusted, and topic identity is untrusted. Production key provisioning, rotation, revocation, and verifier ownership keep the authentication gate a proposal. | Mode | Requirement | Status | | ---------------------------------- | -------------------------------------------------------- | -------------------------------------------------- | | Reachable customer-selected Broker | Dedicated namespace, TLS, challenge/per-message proof | Transport adapter implemented; origin auth planned | | AetherCloud-managed Broker | Same CloudLink application/session boundary | Broker product/principal integration planned | | Private customer Broker | Site connector preserving identity, spool/ACK, and audit | Planned | | Legacy AetherEdge MQTT | Separate namespace, never silently decoded as CloudLink | Retained during migration | MQTT PUBACK is transport evidence only and never authorizes deletion from the AetherEdge spool. ## Evidence and remaining gates The consumer lock pins the complete public manifest: all 25 public fixture bytes execute in both products, and pending imports are empty. Local fixture/gate files are evidence overlays only. | Gate | State | | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | | Shared-Broker message-origin authentication | In progress | | Single envelope/time/identity/digest/unsigned-ACK profile | Implemented alpha.3; production auth remains experimental | | Cross-repository fixtures | Passed for the alpha.3 public manifest | | Real-Broker dual Edge/Cloud harness | Local Mosquitto and AWS IoT Core alpha evidence; authentication gate remains proposal | | ACK loss, restart, duplicate, conflict, and data-loss fault injection | Implemented alpha evidence; process crash durability excluded | | PostgreSQL crash-durable ACK/outbox | Telemetry acceptance PostgreSQL slice implemented and verified; full gate blocked | | Legacy cutover | Blocked until every preceding gate passes | The telemetry acceptance transaction, exact ACK outbox, and bounded delivery use case are implemented. The full crash-durable CloudLink gate remains blocked: production composition is planned, and credential/session persistence, data-loss facts, multi-sample mapping, and alpha.3 production authentication remain incomplete. Legacy remains default. Cloud outage cannot change Edge authority or stop acquisition, rules, alarms, safety interlocks, or local control. There is no physical control in this slice. ## Opt-in dual Edge/Cloud alpha harness ```bash pnpm test:cloudlink-dual ``` The command selects a unique topic prefix, starts and restarts local Mosquitto, runs both product transports, and writes machine-readable evidence to `evidence/cloudlink-alpha3-dual-harness.json` (and a compatibility copy under `artifacts/cloudlink-alpha/evidence.json`). It covers ACK loss, separate Edge process recovery, the Cloud-owned `manifest/1/1` resume cursor, a separate Cloud ingress process restart, exact duplicate replay, digest/batch conflict, explicit data loss, out-of-order, expiry, and a non-durable partial application outcome. It records Cloud restart continuity as unknown; it does not prove production process-crash durability. For current behavior and production limits, read [CloudLink reliability and lifecycle](/en/aethercloud/concepts/cloudlink-and-core-state-machines). For protocol compatibility and version pinning, use the public [AetherContracts compatibility guide](https://docs.aetheriot.workers.dev/aethercontracts/compatibility/). ## Opt-in AWS IoT Core mTLS harness ```bash pnpm test:cloudlink-aws-iot ``` This external-service test requires an authenticated AWS CLI identity with AWS IoT permissions. It defaults to `us-west-2` and can be moved explicitly with `AETHER_CLOUDLINK_AWS_REGION`. The command provisions two separate X.509 client principals and two least-privilege topic policies, creates no Thing, runs the AetherCloud MQTT.js ingress and AetherEdge rumqttc/FileCloudLinkSpool through AWS IoT Core MQTT 3.1.1 on port 8883, and writes sanitized evidence to `evidence/cloudlink-aws-iot-us-west-2.json`. Certificate and private-key bytes remain in a mode-0700 temporary directory; individual files use mode 0600 and do not enter stdout, evidence, Git, or application logs. On every normal success or failure path the harness detaches the policies, deactivates and deletes both certificates, deletes both policies, and removes the temporary directory. The AWS run covers session establishment, heartbeat, Runtime Manifest, telemetry application ACK, ACK-loss replay, duplicate idempotency, conflicting binding, expiry, out-of-order delivery, unsupported partial batch, explicit data loss, and final spool drain. It proves alpha transport interoperability through a managed Broker. It does not pass the production authentication gate: the application does not yet consume reviewed out-of-band AWS principal attestation or verify the planned per-uplink Gateway signatures. Alpha.3 ACKs remain unsigned and Cloud persistence remains process-local memory. --- # HTTP API reference > Authoritative source: [AetherCloud](https://github.com/EvanL1/AetherCloud/blob/main/docs/reference/http-api.md). This page is mirrored into the unified AetherIoT documentation. The repository-foundation milestone exposes two public metadata routes and two authenticated Audit query routes. It does not yet expose fleet, telemetry, provider discovery, deployment, command, webhook/export, or MCP routes. The implemented `DiscoverProviderRegions` application query is intentionally not an HTTP contract until authentication, tenant context, and runtime decoding are composed around it. The implemented `PlanDeploymentStack` application command is also not an HTTP contract. No endpoint accepts a module, topology, Plan artifact, or Apply request. Its future route additionally needs durable audit, encrypted artifact storage, and a production State-locking engine adapter. The implemented Gateway register, enrollment-claim issue/consume, and status use cases are also application contracts only. They remain unexposed until a production Tenant identity boundary, PostgreSQL transaction, durable audit, and secret-backed token service are composed. No endpoint described on this page accepts an enrollment token. ## `GET /health` Returns process liveness without reading PostgreSQL or another external service. Response status: `200 OK` ```json { "status": "ok", "service": "aether-cloud-api", "version": "0.1.0" } ``` This response proves only that the API process can serve requests. Future readiness checks for configured adapters will use a separate endpoint so a database outage does not rewrite liveness semantics. ## `GET /api/v1/platform` Returns stable product-role and authority metadata for clients and coding agents. It contains no tenant data and performs no I/O. Response status: `200 OK` ```json { "name": "AetherCloud", "role": "ai-native-multi-cloud-iot-control-plane", "authority": { "livePointState": "edge", "physicalControl": "edge", "tenantIdentity": "aether-cloud", "desiredRevision": "aether-cloud", "placementPolicy": "aether-cloud", "actualInfrastructure": "provider" }, "multiCloud": { "providerModel": "capability-driven-adapters", "executionEngines": ["opentofu", "terraform"], "stateIsolation": "deployment-stack" } } ``` This is metadata about stable product boundaries, not evidence that a provider connection, inventory route, or infrastructure execution endpoint is already available. ## `GET /api/v1/audit/events` Searches only the Tenant and Project resolved from the authenticated subject. The request cannot supply Tenant scope. It requires `audit.event.read`. Supported optional query fields are `action`, `cursor`, `from`, `limit`, `resourceId`, `resourceKind`, `subjectId`, and `to`. `limit` defaults to 50 and is bounded by the application query. The cursor is the canonical decimal audit sequence; protocol `int64` values remain strings. Response status is `200 OK` with `{ "items": [...], "nextCursor": string | null }`. Each item contains immutable audit subject, action, resource, outcome, risk, confirmation, correlation, and optional trace/evidence-digest fields. ## `GET /api/v1/audit/events/stream` Returns the same authorized query result encoded as `text/event-stream`. The `Last-Event-ID` header resumes after an audit sequence and must agree with a query `cursor` when both are present. Each event id is the audit sequence. This route is a finite resumable snapshot ending with a `snapshot-complete` comment. It is not a durable live subscription and does not prove that a notification worker or broker exists. ## Error shape Versioned application APIs use a stable envelope containing a code, human-readable message, and correlation identity: ```json { "error": { "code": "permission-denied", "message": "permission audit.event.read is required", "correlationId": "request-correlation-id" } } ``` Audit routes return `400 invalid-input`, `401 unauthenticated`, or `403 permission-denied` as applicable and also emit `x-correlation-id`. ## Authentication `/health` and `/api/v1/platform` are intentionally public and contain no Tenant or infrastructure instances. Audit routes require `Authorization: Bearer `. The local server resolves the configured subject from `AETHER_CLOUD_API_BEARER_TOKEN`, `AETHER_CLOUD_API_TENANT_ID`, `AETHER_CLOUD_API_PROJECT_ID`, `AETHER_CLOUD_API_SUBJECT_ID`, and comma-separated `AETHER_CLOUD_API_PERMISSIONS`; missing configuration denies every protected request. Exact tokens are compared in constant time. This configured bearer adapter and the in-memory audit store are suitable for local composition and contract tests, not production identity or durability. Production routes still resolve Tenant context from an authenticated identity before invoking an application use case. A body, path, or query Tenant identity is never proof of access. --- # Repository layout > Authoritative source: [AetherCloud](https://github.com/EvanL1/AetherCloud/blob/main/docs/reference/repository-layout.md). This page is mirrored into the unified AetherIoT documentation. The workspace separates stable product logic from delivery mechanisms. Some directories are established by the repository foundation and others are added when their first vertical slice is implemented. ```text apps/ api/ HTTP composition root mcp/ implemented transport-neutral MCP application interface; wire root planned cloudlink/ experimental MQTT ingress composition; production wiring planned worker/ planned background-work composition root web/ planned operator client packages/ domain/ values, identifiers, invariants application/ transport-neutral use cases provider-conformance/ reusable Provider Adapter contract tests infrastructure-conformance/ reusable Infrastructure Engine contract tests adapters/ fleet/memory/ implemented Gateway repository/token test adapters fleet/postgres/ implemented Gateway SQL/migration/driver adapter; production composition planned cloudlink/memory/ implemented session and heartbeat test adapter cloudlink/mqtt/ experimental strict codec and MQTT.js transport adapter runtime/memory/ implemented Runtime Manifest history test adapter telemetry/memory/ implemented atomic telemetry conformance adapter alarm/memory/ implemented alarm projection conformance adapter artifacts/memory/ implemented Artifact Registry conformance adapter deployment/memory/ implemented edge deployment conformance adapter jobs/memory/ implemented governed Job ledger conformance adapter audit/memory/ implemented append-only Audit query test adapter integration/memory/ implemented webhook subscription/delivery and export test adapters observability/opentelemetry/ optional implemented operational-signal adapter providers/memory/ deterministic implemented test adapter providers// planned SDK discovery and normalization adapters infrastructure/memory/ implemented deterministic Plan engine and repository infrastructure/opentofu/ implemented real local Plan-only CLI adapter infrastructure/terraform/ planned Terraform CLI adapter persistence/ planned shared migration runner and object-store adapters infra/modules/ planned versioned provider-specific IaC modules contracts/cloudlink/ experimental JSON Schemas and golden fixtures ai/ invariants and machine-readable documentation catalog skills/aether-cloud/ coding-agent workflow and routing docs/ concepts, guides, reference, and decisions tests/ repository-wide contract tests ``` ## Placement rules Put a type in `domain` when it expresses business meaning without I/O. Put an interface in a port-owning application module when it describes a capability needed by a use case. Put orchestration in `application`. Put Fastify routes, process signals, environment access, and dependency construction in an app. An adapter may depend on a port and third-party SDK. A port cannot depend on its adapter. Do not create a generic utilities package; place a helper with the concept that owns its semantics. Provider-neutral intent belongs in domain and application packages. Provider-specific modules, SDK clients, authentication, and normalization stay under adapters and `infra/modules`. A worker composition root selects a Provider Adapter and an `InfrastructureEngine`; neither is selected inside a domain use case. ## Package exports Packages expose deliberate public entry points. Import another package through its declared export rather than reaching into its internal directory. Avoid barrel exports that accidentally make persistence records or framework types part of the application contract. ## Adding a bounded context Start with one observable use case and its domain language. Add the test and application contract before choosing persistence. If the context needs an adapter, provide an in-memory conformance implementation so default tests stay self-contained. Update [architecture](/en/aethercloud/concepts/architecture) only when the context changes system responsibilities or dependencies; routine file additions belong in this reference page or a package README. --- # Terminology > Authoritative source: [AetherCloud](https://github.com/EvanL1/AetherCloud/blob/main/docs/reference/terminology.md). This page is mirrored into the unified AetherIoT documentation. **Applied revision** — A revision an edge reports as successfully activated. It is not inferred from a download or cloud request. **Capability** — A typed query or command exposed through the application layer, with permission and safety metadata. **Capability job** — Durable cloud intent for an edge capability, including expiry, idempotency, preconditions, authorization evidence, and audit context. **Cloud connection** — Tenant-scoped configuration that selects one explicit provider identity, account or subscription scope, and secret reference. **CloudLink** — The versioned, authenticated protocol between AetherCloud and an AetherEdge runtime. It is not a device protocol. **Control-plane cell** — One independently operated API, CloudLink, worker, PostgreSQL, and object-storage placement with one authoritative transactional writer topology. A Tenant has an explicit home cell. **Connected** — A gateway has a sufficiently recent authenticated CloudLink session. This does not imply that downstream devices are online. **Desired revision** — The cloud-authored target revision for a gateway or deployment group. **Deployment stack** — The smallest provider-scoped unit that is planned, locked, applied, recovered, and destroyed independently, with one remote State. Its isolation key is derived from its Tenant, Project, Cloud Connection, and Stack identities. **Edge** — One commissioned AetherEdge runtime and its local authority. Edge and gateway are related but not interchangeable in protocol contracts. **Enrollment claim** — A Tenant/Project/Gateway-bound, expiring bootstrap claim used to bind a registered Gateway to credential-request evidence. It is not a long-lived API or CloudLink credential. **Gateway credential generation** — One independently revocable generation of long-lived Gateway identity material. Recovery creates a new generation. **Gateway** — The cloud resource representing an enrolled AetherEdge runtime identity. **Instance** — A thing-model instance owned by one gateway. **Infrastructure engine** — An adapter that plans versioned provider modules. OpenTofu is the default; Terraform is compatible through the same application port. The implemented port deliberately exposes no Apply operation. **Saved Plan** — An engine-generated, immutable proposal for one Deployment Stack and one State. AetherCloud retains an encrypted artifact reference, digest, lock evidence, version, change summary, and policy result; approval or Apply is a separate fact. **Plan artifact** — The encrypted external object containing an engine's saved Plan. Its binary and raw JSON can contain sensitive values and never belong in logs, prompts, audit payloads, or normalized records. **Placement** — AetherCloud's desired choice of provider connection, region, and constraints for a workload. It is not proof that resources exist. **Point** — A typed measurement, status, command, or adjustment member of an instance. **Projection** — An AetherCloud representation derived from an authoritative edge or provider fact, such as telemetry history, applied state, or cloud inventory. **Provider Adapter** — A capability-driven plugin that owns provider authentication, discovery, provider-specific modules, normalized observations, and conformance evidence. **Provider database profile** — Provider-specific evidence for a managed or self-hosted PostgreSQL placement, including version, HA, backup, encryption, network, extension, replica, region, RPO/RTO, cost, and native extensions. It is not the application persistence contract. **Reported state** — A time-stamped edge observation. It may become stale and does not overwrite desired state. **Site** — A durable physical or logical location that can contain gateways. **State** — An infrastructure engine's durable mapping between a deployment stack and provider resources. It is remote, locked, versioned, encrypted, and never shared across provider connections. **Tenant** — The primary security and data-isolation boundary. Use the [resource model](/en/aethercloud/concepts/resource-model) for ownership and identity rules. --- # Product naming migration > Authoritative source: [AetherContracts](https://github.com/EvanL1/AetherContracts/blob/main/MIGRATION.md). This page is mirrored into the unified AetherIoT documentation. AetherIoT is the umbrella project for AetherEdge, AetherCloud, and AetherContracts. The edge product and repository formerly named AetherIot move to `EvanL1/AetherEdge`. AetherContracts `v0.1.0-alpha.3` is immutable and intentionally retains the historical AetherIot name in its signed, digest-pinned release artifacts. Consumers must not rewrite those bytes. Future contract releases may use the AetherEdge display name while preserving every protocol, Schema, TCK, package, and failure-code identifier unless a separate versioned contract decision says otherwise. Repository renaming is not protocol evolution and does not change conformance status. The alpha.3 release remains experimental and is not a production CloudLink cutover release. --- # AetherContracts compatibility and release gates > Authoritative source: [AetherContracts](https://github.com/EvanL1/AetherContracts/blob/main/docs/compatibility.md). This page is mirrored into the unified AetherIoT documentation. Compatibility is evidence-based. Sharing a version string or successfully decoding one fixture does not prove complete interoperability. The current `v0.1.0-alpha.3` release freezes an experimental common contract while keeping legacy transport as the default. ## Current product baseline | Product | Contract relationship | Current status | | --- | --- | --- | | AetherEdge | Digest-pinned complete alpha.3 consumer; strict Rust codec and MQTT transport foundation | Experimental consumer evidence | | AetherCloud | Digest-pinned complete alpha.3 consumer; strict TypeScript codec, MQTT ingress, and accepted-telemetry ACK slice | Experimental consumer evidence | | Independent implementations | Exact release archive, closed consumer lock, public fixtures, and TCK | Supported distribution path; conformance must be proven by the consumer | Both product consumers import the same required artifact closure and execute the same public fixture outcomes. This proves distribution integrity and a shared experimental core. It does not prove production identity, complete state-machine behavior, crash durability, or safe legacy cutover. ## CloudLink gate status | Gate | Status | Meaning | | --- | --- | --- | | Shared-Broker authentication | Proposal | The transcript is frozen, but production key provisioning, rotation, revocation, and verifier ownership are not implemented | | Single wire contract | Experimental | Core envelope, time, identity, digest, and ACK semantics have one public authority | | Cross-language fixtures | Passed, experimental | TypeScript, Rust, C, and C++ execute the same fixture manifest and stable failure classes | | Real-Broker dual harness | Consumer evidence required | Products must prove concurrent edge and cloud behavior through application use cases | | Fault injection | Consumer evidence required | Disconnect, ACK loss, restart, duplicate, conflict, and data-loss outcomes require product evidence | | Signed durable ACK | Planned | The signing projection, key lifecycle, and production fact transaction remain open | | Legacy cutover | Blocked | Every preceding gate must pass and rollback must remain available | The machine-readable authority is [`compatibility/cloudlink-v1alpha1-gates.json`](https://github.com/EvanL1/AetherContracts/blob/main/compatibility/cloudlink-v1alpha1-gates.json). ## Binding compatibility | Binding | Implemented alpha.3 surface | Not yet claimed | | --- | --- | --- | | TypeScript | Canonical `uint64`, JSON canonicalization, public fixture manifest | Complete production Schema and transport codec | | Rust | Full-range canonical `u64`, typed failures, public fixture manifest | Complete production JSON, model, and transport codec | | C99 | Bounded canonical `uint64`, allocation-free P/M/A lookup, bounded fixture profile | Complete production JSON, model, and transport codec | | C++17 | Thin views and results over the C99 core | Independent wire semantics or a second codec | | Go, Java, Python | Planned | No conformant binding is currently published | Stable string failure codes are contractual. Numeric error values and message text remain binding-specific. Read the machine-readable [`compatibility/failure-codes.json`](https://github.com/EvanL1/AetherContracts/blob/main/compatibility/failure-codes.json) before mapping errors or retry behavior. ## Compatibility rules - Protocol `uint64` values use canonical decimal strings. - Core JSON objects are closed and reject unknown fields. - Duplicate keys, invalid Unicode, unsafe numbers, and unbounded input fail closed. - MQTT acknowledgement is transport evidence, never durable application acceptance. - Thing Model capabilities are declarations, not authority grants. - CloudLink contains no direct physical-control operation. - A later encoding requires explicit negotiation and its own TCK. Every future release should publish exact product versions or commits and link to executable evidence. Floating `main`, `latest`, and implied compatibility are not release evidence. --- # Conformance and consumer verification > Authoritative source: [AetherContracts](https://github.com/EvanL1/AetherContracts/blob/main/docs/conformance.md). This page is mirrored into the unified AetherIoT documentation. AetherContracts separates four kinds of evidence so that structural acceptance, contextual behavior, language APIs, and product integration are not confused. ## Evidence layers 1. **Specification:** normative English semantics and lifecycle rules. 2. **Schema:** closed JSON Schema Draft 2020-12 structural acceptance. 3. **Fixture and TCK:** valid, invalid, migration, and contextual outcomes with stable public failure classes. 4. **Consumer evidence:** product-specific transport, persistence, restart, authentication, and fault behavior. The first three layers belong to AetherContracts. The fourth belongs to each consumer. A product cannot modify a local wire file and claim that it changes the public contract. ## Run the language-neutral TCK ```bash pnpm test:tck ``` The black-box runner validates bounded parsing, canonical integer behavior, Thing Model migration, CloudLink fixture outcomes, contextual replay and cursor rules, and manifest consistency. It is offline by default. Read [TCK v1 alpha](/en/aethercontracts/spec/tck-v1alpha1) for the runner contract and [Foundation](/en/aethercontracts/spec/foundation) for common failure semantics. ## Binding evidence Each binding must execute the same public fixture manifest and report the same contractual string failure class: ```bash pnpm test:typescript pnpm check:rust pnpm check:c ``` The complete repository check also verifies packaging, the CMake installation, sanitizer behavior, generated artifacts, and release hashes: ```bash pnpm check ``` Passing these checks means the published alpha surface behaves consistently. It does not claim that every binding is a complete production codec. ## Verify a consumer closure A consumer lock identifies the exact release tag, peeled commit, manifest digest, imported artifact set, and pending set. A complete consumer must import the entire required closure with no pending artifacts. The release composite Action and offline verifier reject: - a tag or Action commit that does not match the lock; - an archive with an unsafe or unexpected layout; - a manifest, artifact, or imported byte with the wrong digest; - an incomplete or extra adoption closure; - a local authority file that attempts to override the public release. Online verification authenticates the GitHub release identity before the same local byte checks. Offline verification is the default for already imported consumer trees and does not contact a registry, Broker, or cloud account. ## Product evidence remains separate AetherEdge and AetherCloud add Real-Broker, restart, PostgreSQL, and fault evidence in their own repositories. Those results may satisfy a release gate, but they do not mutate the AetherContracts tag. Likewise, passing the public TCK does not prove a product's key lifecycle, durable outbox transaction, operational deployment, or rollback path. Review [compatibility and release gates](/en/aethercontracts/compatibility) before calling an implementation conformant or changing a legacy transport default. --- # Get started with AetherContracts > Authoritative source: [AetherContracts](https://github.com/EvanL1/AetherContracts/blob/main/docs/getting-started.md). This page is mirrored into the unified AetherIoT documentation. AetherContracts is the public interoperability authority shared by AetherEdge, AetherCloud, and independent implementations. Start from an exact release and keep its specification, Schemas, fixtures, TCK, and manifest together. A single copied Schema or generated type is not a complete contract adoption. The current release is `v0.1.0-alpha.3`. It is experimental, keeps legacy transport as the default, and is not a production CloudLink cutover release. ## Choose the contract surface | Need | Read or run first | | --- | --- | | Common JSON, integer, canonicalization, and failure rules | [Foundation](/en/aethercontracts/spec/foundation) | | Thing Model structure and P/M/A migration | [Thing Model v1 alpha](/en/aethercontracts/spec/thing-model-v1alpha1) | | CloudLink messages and lifecycle | [CloudLink v1 alpha](/en/aethercontracts/spec/cloudlink-v1alpha1) | | Release distribution and consumer locks | [Distribution v1 alpha](/en/aethercontracts/spec/distribution-v1alpha1) | | Executable conformance behavior | [TCK v1 alpha](/en/aethercontracts/spec/tck-v1alpha1) | | Current gates and product compatibility | [Compatibility](/en/aethercontracts/compatibility) | | Binding and consumer evidence | [Conformance](/en/aethercontracts/conformance) | Normative specifications define semantics. JSON Schemas define structural acceptance. Fixtures pin examples and stable failure outcomes. The TCK proves observable behavior. Language bindings implement that contract but never become a second authority. ## Verify a source checkout Use Node.js 24 and the repository-declared pnpm version: ```bash git clone https://github.com/EvanL1/AetherContracts.git cd AetherContracts git checkout v0.1.0-alpha.3 corepack enable pnpm install --frozen-lockfile pnpm test:tck ``` `pnpm test:tck` is self-contained. It does not require a Broker, database, cloud account, or edge device. Run `pnpm check` when changing the repository or validating all TypeScript, Rust, C, and C++ binding foundations. ## Adopt an exact release Production-oriented consumers should not follow a floating branch or copy an unverified subset. Commit a closed `aether-contracts.lock.json`, import the exact required artifact closure, and run the release's composite verification Action. The verifier checks the peeled release commit, manifest digest, artifact hashes, adoption closure, and optional online release identity. The checked-in consumer copies in AetherEdge and AetherCloud demonstrate this distribution model. Their alpha.3 evidence does not upgrade authentication, durable acknowledgement, or legacy cutover to production status. ## Select a binding - [TypeScript](/en/aethercontracts/packages/typescript) - [Rust](/en/aethercontracts/packages/rust) - [C99](/en/aethercontracts/packages/c) - [C++17](/en/aethercontracts/packages/cpp) All four bindings execute the public fixture manifest. They are intentionally narrow foundations rather than complete production transport codecs. Go, Java, and Python bindings remain planned. If an existing consumer still refers to the former edge repository name, read the [AetherEdge naming migration](/en/aethercontracts/migration). Package and protocol identifiers remain stable. --- # AetherContracts AetherContracts is the public, language-neutral interoperability authority for AetherEdge, AetherCloud, and independent implementations. Specifications define behavior, JSON Schema Draft 2020-12 defines structural acceptance, fixtures pin observable examples, and the black-box TCK supplies executable evidence. A product-local copy or language binding never becomes a second source of truth. ## Current release `v0.1.0-alpha.3` freezes an experimental CloudLink wire/profile/TCK surface and provides TypeScript, Rust, C, and C++ fixture bindings. It is not a production CloudLink cutover release. The release preserves the historical AetherIot consumer name in signed and digest-pinned artifacts. Those bytes remain immutable after the repository is renamed to AetherEdge. Later releases may update product display names without changing protocol identifiers. Read the [AetherContracts repository](https://github.com/EvanL1/AetherContracts), the [compatibility matrix](/en/compatibility/version-matrix), and the [Edge to Contracts to Cloud tutorial](/en/tutorials/edge-contracts-cloud). --- # AetherContracts C foundation > Authoritative source: [AetherContracts](https://github.com/EvanL1/AetherContracts/blob/main/packages/c/README.md). This page is mirrored into the unified AetherIoT documentation. The C99 foundation is allocation-free and uses caller-owned string views. It currently provides canonical `uint64` parsing plus static Thing Model property/point/capability lookup. Capability metadata is deny-by-default and has no invocation API. A bounded, caller-owned experimental validator executes the public CloudLink fixture profile. It is not a general JSON parser or a complete production CloudLink transport/authentication codec. Install or consume the root CMake project and link `AetherContracts::c`. --- # AetherContracts C++ foundation > Authoritative source: [AetherContracts](https://github.com/EvanL1/AetherContracts/blob/main/packages/cpp/README.md). This page is mirrored into the unified AetherIoT documentation. The C++17 target `AetherContracts::cpp` is a thin, non-owning wrapper over the C99 core. It does not implement a second parser or protocol contract. Full JSON and production CloudLink conformance remain planned; its experimental fixture surface delegates to the C99 core. --- # aether-contracts > Authoritative source: [AetherContracts](https://github.com/EvanL1/AetherContracts/blob/main/packages/rust/README.md). This page is mirrored into the unified AetherIoT documentation. Experimental Rust foundation binding for AetherContracts. It currently provides canonical `u64` parsing with language-neutral failures and executes the public CloudLink fixture manifest. It is not a complete production Thing Model, authentication, or transport codec. --- # @aether-contracts/typescript > Authoritative source: [AetherContracts](https://github.com/EvanL1/AetherContracts/blob/main/packages/typescript/README.md). This page is mirrored into the unified AetherIoT documentation. Experimental TypeScript foundation binding for AetherContracts. It currently provides canonical `uint64` parsing and RFC 8785-compatible JSON canonicalization, plus an experimental validator that executes every entry in the public CloudLink fixture manifest with stable failure strings. It is not a complete production Thing Model, authentication, or transport codec. Normative contracts remain in the repository release bundle. --- # CloudLink v1 alpha 1 > Authoritative source: [AetherContracts](https://github.com/EvanL1/AetherContracts/blob/main/spec/cloudlink-v1alpha1.md). This page is mirrored into the unified AetherIoT documentation. AetherContracts is the sole interoperability authority for this protocol slice. Product-repository wire profiles, manifests, gates, and evidence files are non-authoritative implementation overlays and may not add or redefine wire fields. The historical joint-core provenance records only where alpha.2 input bytes came from; it grants no continuing joint ownership. Alpha.3 freezes the closed envelope and time/identity fields, session challenge/hello/acceptance, heartbeat, Runtime Manifest report, telemetry batch, data-loss evidence, replay request, and an unsigned durable application ACK. TypeScript, Rust, C99, and C++17 execute the same public fixture manifest and stable failure strings. Those fixture surfaces are experimental and are not complete production transport codecs. The authentication profile distinguishes two origin models. In `gateway-signed`, Cloud issues a one-time signed challenge, the Gateway signs the exact session-establishment object, and every uplink signs the exact per-uplink object. In `trusted-connector-broker-attestation`, a configured trusted ingress adapter supplies origin evidence outside the payload and binds the exact received MQTT payload bytes. A payload cannot attest to itself. Topic names, payload identity, and MQTT credentials alone are never Gateway authentication. The proposal specifies the Ed25519 algorithm, unpadded-base64url encoding, RFC 8785 JCS signing objects, absent-value rules, and replay bounds exactly in `profiles/cloudlink/v1alpha1/authentication.json`. Production key provisioning, rotation, revocation, verifier ownership, and production signature verification remain planned, so the authentication gate remains a proposal and cutover is blocked. Ordinary logs and public evidence must exclude signatures, nonces, credential identifiers, and raw authentication transcripts. The alpha.3 durable-ACK JSON shape is explicitly unsigned. Success means the application fact and receipt were durably committed before ACK publication, but alpha.3 contains no production store/outbox implementation and makes no crash-durability claim. A future signed ACK is a separate command/profile and requires a Cloud key lifecycle plus production restart evidence. MQTT PUBACK is never an application durable receipt. Repeated delivery with the same replay identity and digest is idempotent. Reuse of an identity with a different digest is wire-valid but context-invalid; it is quarantined as `DIGEST_CONFLICT` and receives no successful receipt. Data loss is explicit evidence and never causes Cloud to fabricate samples. Data-loss evidence satisfies `first_lost_position <= last_lost_position < earliest_retained_position`. Heartbeat and resume cursor arrays contain at most one entry for each `(stream_id, stream_epoch)`. Violations are context-invalid, do not change a business fact, and do not permit a successful application receipt. The only durable position identity is `(gateway_id, stream_id, stream_epoch, position)`. `batch_id` and `digest` are stable bindings of that position, not fields that create a second identity; changing either cannot bypass conflict detection. A business digest is the lowercase SHA-256 of RFC 8785 JCS over exactly `{protocol_version,message_kind,payload}`. It provides content integrity and replay comparison, not publisher authentication. The machine-readable form is `profiles/cloudlink/v1alpha1/core.json`. `expires_at_ms` is optional; when omitted, that field imposes no message deadline. When present, it must be greater than or equal to `sent_at_ms`, or the message is context-invalid with `INVALID_EXPIRY_WINDOW`. Expiration is evaluated against an explicit canonical uint64 `evaluation_time_ms`: the check passes only while `evaluation_time_ms < expires_at_ms`, and equality is already expired with `MESSAGE_EXPIRED`. The portable TCK supplies this evaluation time as scenario input and never consults an ambient wall clock. Both expiry failures leave business state unchanged and forbid a successful application receipt. The embedded Runtime Manifest checksum is lowercase SHA-256 over RFC 8785 JCS of the complete manifest object with its top-level `checksum` member omitted. Its digest omits the `sha256:` prefix because the enclosing checksum object already declares the algorithm. `envelope.schema.json` is the reusable structural base. Consumers must validate an uplink with its message-kind entry Schema (`runtime-manifest-report`, `telemetry-batch`, or `data-loss`); using the base alone does not validate the discriminator-to-payload relationship. The alpha telemetry slice currently carries only finite JSON-number values for telemetry/status points. Non-numeric Thing Model value types, events, and the topology-to-model point-resolution contract are planned, not silently inferred. An optional sample `model` value is only a commissioning hint and is not sufficient mapping authority. This slice does not freeze exact signed `int64`, `uint64`, decimal, byte, or string sample encodings. A JSON number is not a substitute for an exact 64-bit integer contract. Legacy transport remains the default. CloudLink contains no physical-control, direct SHM, or direct register operation. --- # Consumer distribution v1 alpha 1 > Authoritative source: [AetherContracts](https://github.com/EvanL1/AetherContracts/blob/main/spec/distribution-v1alpha1.md). This page is mirrored into the unified AetherIoT documentation. This profile lets AetherCloud, AetherIot, and independent implementations pin one exact AetherContracts release while keeping their default verification path offline. Distribution conformance proves release identity and byte integrity; it does not prove that a consumer codec, state machine, authentication profile, or Broker integration is conformant. ## Authority and trust The reviewed `aether-contracts.lock.json` in a consumer repository is the local trust decision. It pins the release version, annotated tag object, peeled commit, exact release URL, bundle size and SHA-256, and external manifest SHA-256. The tag object and commit provide reviewable provenance identifiers. The bundle and manifest digests provide the enforced byte identity. GitHub tags, releases, and co-hosted checksum files are mutable distribution evidence, not a second trust root. This alpha does not yet require a Sigstore or SLSA attestation that cryptographically binds the bundle to the commit. A cache or CDN may serve only bytes already accepted by the lock digest. Consumers must not follow `main`, `latest`, a version range, or an unpinned action revision. ## Lock behavior The lock is closed by `schemas/distribution/v1alpha1/consumer-lock.schema.json`. Its safety policy is fixed for this experimental line: - `conformance_claim` is `distribution-only`; - `production_release` is `false`; - `legacy_default` is `true`; - `physical_control` is `false`. An `import` binds one release artifact path, one consumer destination path, and one SHA-256. A `pending_import` records a release artifact that the consumer has not adopted and a non-empty reason. Imported and pending source sets are disjoint. The lock's `adoption` section declares the scope, required modules, and exact required release-source closure. The union of imported and pending sources must equal that closure. A `partial-consumer` has at least one pending source; a `complete-consumer` has none and imports the entire closure. Complete distribution adoption still says nothing about behavioral conformance. The consumer commits the exact release `contract-manifest.json` bytes at the lock's `manifest.local_path`. Offline verification checks its digest, release identity, safety declarations, artifact declarations, and every imported consumer byte. It performs no download, repair, fallback, or write. ## Online verification The optional online verifier downloads only the URL named by the lock. It enforces the exact response size and SHA-256 before inspection. It parses gzip and tar in-process under the lock's maximum compressed bytes, expanded bytes, entry count, path bytes, per-file bytes, and total regular-file bytes. It rejects absolute or escaping paths, links, devices, unsupported entry types, duplicate normalized paths, invalid checksums, malformed terminators, and any archive layout other than the one locked release root. Extraction uses private directories and files and occurs only after validation. It then verifies the manifest plus every imported and pending release-source byte. Failure is terminal; there is no fallback to a sibling checkout or consumer-local candidate. Consumers must pin `.github/actions/verify-consumer` to the full 40-character peeled commit of the locked release. The composite Action passes its actual action commit to the verifier, which rejects a different revision with `ACTION_COMMIT_MISMATCH`. The lock path is consumer-relative and must remain inside the consumer repository. This check complements, but never replaces, the consumer's native codec and state-machine conformance tests. --- # Contract foundation > Authoritative source: [AetherContracts](https://github.com/EvanL1/AetherContracts/blob/main/spec/foundation.md). This page is mirrored into the unified AetherIoT documentation. The v1 alpha logical representation and wire encoding are UTF-8 JSON. Closed core objects reject unknown fields. A conformant raw decoder must also reject duplicate object keys, malformed Unicode, input beyond the profile limits, and JSON numbers that cannot be represented without the contract's declared semantics. Protocol `uint64` values are canonical decimal strings. They have no sign, whitespace, decimal point, exponent, or leading zero, except that zero is exactly `"0"`. The maximum `uint64` is `"18446744073709551615"`. Thing Model may declare an `int64` value type, but its signed wire representation and TCK are planned and are not frozen by this alpha foundation. Validation order is contractual: after checking the binding input type, a representation longer than 20 bytes fails as `INTEGER_OUT_OF_RANGE` before digit syntax is inspected. This keeps allocation-free C decoders bounded and gives every language the same failure for overlength adversarial input. JSON Schema acceptance is necessary but not sufficient. Ordering, replay, identity conflicts, authorization, revision compatibility, and durable receipt semantics require contextual validators and TCK scenarios. JSON numbers use finite IEEE 754 binary64 semantics. If the decoded value is integer-valued but outside the interoperable safe-integer range `[-9007199254740991, 9007199254740991]`, it is accepted only when its RFC 8785 serialization retains a decimal point; otherwise the producer must use a contract-declared decimal string. Thus `1e100` and `1.5e20` fail as `JSON_UNSAFE_NUMBER`, while `1e-100` and the binary64 maximum `1.7976931348623157e308` remain floating-point values. Protocol integers never use this exception: they use their explicitly frozen string encoding. The TCK strict decoder applies defensive reference-runner budgets for nesting, strings, collections, and number tokens. Those defaults are implementation safety limits, not portable acceptance maxima. A transport or artifact profile freezes portable limits where interoperability requires them; the MQTT profile currently freezes the complete message at 262144 bytes. Other bindings may apply documented stricter resource limits and must fail closed with `FIELD_BOUND`. When a contract defines a signed object, the exact signing projection must be specified before use. It is serialized with RFC 8785 JSON Canonicalization, hashed with SHA-256, and then signed. Pretty-printed source bytes are never a signature input. This alpha release does not freeze the CloudLink authentication or signed-ACK projection. Stable string failure codes are language-neutral. Numeric values and error text are binding details until a future ABI profile explicitly freezes them. --- # Language-neutral TCK v1 alpha 1 > Authoritative source: [AetherContracts](https://github.com/EvanL1/AetherContracts/blob/main/spec/tck-v1alpha1.md). This page is mirrored into the unified AetherIoT documentation. Repository tests compile every normative JSON Schema, validate valid and wire-invalid fixtures, preserve context-invalid distinctions, and verify every declared SHA-256. The repository reference runner executes the published core scenarios for integer precedence, strict raw JSON, business and Runtime Manifest digests, minimal session/replay/ACK context, data-loss and cursor rules, and Thing Model key conflicts. It is not a production CloudLink state machine. Wire-invalid fixtures currently prove structural rejection; exact Schema-to-failure-code and JSON-path mapping remains planned. The portable black-box runner protocol is planned as NDJSON over standard input and output. Operations are `validate`, `canonicalize`, `digest`, `verify-signature`, and `check-compatibility`. It will compare acceptance, stable failure code, JSON path, canonical bytes, digest, and state outcome; it will not compare language-specific error prose. TypeScript, Rust, C, and C++ remain experimental until each executes the same manifest and scenario set. The real-Broker dual harness and destructive fault injection are separate opt-in evidence and never enter the default offline test path. --- # Thing Model v1 alpha 1 > Authoritative source: [AetherContracts](https://github.com/EvanL1/AetherContracts/blob/main/spec/thing-model-v1alpha1.md). This page is mirrored into the unified AetherIoT documentation. A Thing Model is an immutable, industry-neutral definition. It describes configuration properties, edge-observed points, and governed capabilities. It does not contain tenant instances, live values, credentials, register bindings, or customer topology. Every published revision has one `model_id`, canonical positive `revision`, and canonical artifact digest. The digest belongs to the external publication record because a self-hashed Thing Model cannot contain its own digest; it is therefore deliberately absent from `thing-model.schema.json`. Reusing a revision for different canonical bytes is forbidden. Keys must be unique across the property, point, and capability namespaces; this is a semantic validation rule in addition to JSON Schema. Parameter keys must also be unique within each capability so request payloads cannot have ambiguous meanings. The publication digest is `sha256:` plus lowercase SHA-256 of RFC 8785 JCS over the complete Thing Model object accepted by `thing-model.schema.json`. No publication wrapper or tenant metadata enters that projection. The machine-readable rule is `profiles/thing-model/v1alpha1/publication.json`. ## Authority - Properties are owned by an immutable artifact revision. Changes are applied by an artifact deployment or by an explicitly edge-local path, never by a direct field write. - Points are edge-authoritative, read-only observations. Cloud history and projections are not live-state authority. - Capabilities declare what an edge may understand. Every capability is deny-by-default, requires a permission, risk class, confirmation policy, idempotency, expiry, and audit policy, and executes only as a governed job. The edge makes the final accept, reject, expire, or apply decision. Desired, Reported, and Applied are distinct facts. Desired is Cloud intent; Reported is an edge observation of support; Applied is edge-reported deployment evidence. None may be inferred from another solely because a message was sent or a transport acknowledgement arrived. An `applied` observation carries a non-null model reference and `applied_at_ms`. `not-applied` carries a null model and no applied timestamp; `applying` and `failed` identify the attempted model but do not fabricate an apply time. The root `observed_at_ms` timestamps every observation state. ## Voltage migration profile The structural migration is: - `P` becomes `properties`. - `M` becomes `points`. - `A` becomes `capabilities`. - `pName` becomes taxonomy or composition metadata outside the core model. - legacy numeric IDs are retained as namespaced aliases, never as global IDs. Unit spellings are normalized while the legacy spelling is retained as provenance. Ambiguous source types, arrays represented as scalars, duplicate meaning, and uncertain units require an explicit migration diagnostic; an importer must not guess silently. The minimal fixture is structurally informed by `EvanL1/voltage-product-lib` at commit `7c4eec680f8b5e9a76a57c08078a41b9d5b4550c`. The source repository had only a README statement claiming MIT and no standalone LICENSE at import time, so the catalog was not copied. The fixture is not a normative energy model pack. ## WoT relationship The vocabulary is aligned with W3C Web of Things properties, events, actions, and reusable Thing Models for future import/export. JSON-LD processing is not a v1 alpha core requirement. A WoT action maps only to an Aether capability declaration and never grants direct physical execution. --- # AetherEdge AetherEdge is the open-source, industry-neutral Linux edge runtime, Kernel, CLI, and Rust SDK formerly published from the AetherIot repository name. ## Implemented today - Six isolated runtime services for acquisition, automation, alarms, history, the application API, and uplink. - Shared-memory authority for current point and health state. - Embedded SQLite desired state, history, audit, and durable local outbox. - The `aether` CLI, governed HTTP and MCP application boundaries, Domain Packs, and the `aether-edge-sdk` facade. - A signed `v0.5.0` source, runtime, installer, and CLI release. ## Experimental today - Broker-neutral CloudLink MQTT v1 sessions, telemetry, replay, and application acknowledgement spooling. - Digest-pinned AetherContracts `v0.1.0-alpha.3` consumption and public fixture execution. Experimental CloudLink evidence does not establish production authentication, signed acknowledgement, or end-to-end crash durability. Legacy MQTT remains the compatibility default. ## Stable compatibility names The repository display name changes to AetherEdge. Existing crate names, binary names, the `aether` CLI, `aether-edge-sdk`, configuration keys, service identities, installer names, and protocol identifiers do not change in this migration. Start with the [Agent Quickstart](/agent-quickstart), [Getting Started](/en/guides/getting-started), or the [migration guide](/en/migration/aetheriot-to-aetheredge). --- # Agent Quickstart This page is written for an AI agent driving a shell, not a human reading prose. Each step states the command and the exact signal that means "this step succeeded, move on." ## 1. Install the AetherEdge Skill From the application repository where the agent will work: ```bash npx skills add EvanL1/AetherEdge -s aether-iot ``` Restart the coding assistant if it does not reload project Skills automatically. **Success criterion:** the assistant lists `aether-iot` as an available Skill. ## 2. Install the `aether` CLI Building from a source checkout is the most direct development path: ```bash cargo build --release -p aether sudo cp target/release/aether /usr/local/bin/aether ``` See [Getting Started](/en/guides/getting-started/) for prerequisites if the build fails. Once a tagged release exists, a prebuilt binary is faster — download from GitHub Releases, verify its checksum, and extract it. Pick the asset matching your platform: | Platform | Asset | |---|---| | Linux arm64 | `aether-linux-aarch64.tar.gz` | | Linux x86_64 | `aether-linux-x86_64.tar.gz` | | macOS arm64 | `aether-darwin-aarch64.tar.gz` | | Windows x86_64 | `aether-windows-x86_64.zip` | ```bash REPO="EvanL1/AetherEdge" ASSET="aether-linux-x86_64.tar.gz" # substitute your platform's asset name TAG=$(curl -fsSL "https://api.github.com/repos/$REPO/releases/latest" \ | grep -m1 '"tag_name"' | cut -d '"' -f4) curl -fsSLO "https://github.com/$REPO/releases/download/$TAG/$ASSET" curl -fsSLO "https://github.com/$REPO/releases/download/$TAG/$ASSET.sha256" shasum -a 256 -c "$ASSET.sha256" tar xzf "$ASSET" chmod +x aether sudo mv aether /usr/local/bin/aether ``` **Success criterion:** `aether --version` prints a version string and exits 0. ## 3. Plan and apply the first-run configuration ```bash aether --json setup ``` Read `data.plan_id` from the JSON output. Then, without modifying anything about the site in between, apply that exact plan: ```bash aether setup apply --plan-id ``` **Success criterion:** the apply command's JSON envelope has `"success": true` and exit code 0. This never starts a service or enables a device — it only creates the safe-empty configuration and local SQLite state. ## 4. Start the services Aether's default deployment is Docker Compose. Generate the two required first-start secrets, then bring the stack up: ```bash cp .env.example .env chmod 600 .env export JWT_SECRET_KEY="$(openssl rand -hex 32)" export AETHER_BOOTSTRAP_ADMIN_PASSWORD="$(openssl rand -hex 32)" sed -i.bak \ -e "s/^JWT_SECRET_KEY=.*/JWT_SECRET_KEY=${JWT_SECRET_KEY}/" \ -e "s/^AETHER_BOOTSTRAP_ADMIN_PASSWORD=.*/AETHER_BOOTSTRAP_ADMIN_PASSWORD=${AETHER_BOOTSTRAP_ADMIN_PASSWORD}/" \ .env && rm .env.bak unset JWT_SECRET_KEY AETHER_BOOTSTRAP_ADMIN_PASSWORD aether services start ``` **Success criterion:** `aether --json services status` reports all requested services as running. See [Deployment](/en/guides/deployment/) if the the compatibility `aetherems:latest` runtime image doesn't exist yet on this machine — it needs to be built or loaded before `services start` can succeed. The retained image name does not make the EMS product part of this repository. ## 5. Verify health ```bash aether --json doctor ``` **Success criterion:** the envelope is `{"success": true, ...}` and the process exits 0. `doctor` checks the Docker engine, all six core services' health routes, the SQLite database, the four required config files, and the shared-memory segment — a `false`/non-zero result means one of those failed; read the JSON `error` field for which one. ## 6. Connect an MCP client ```bash claude mcp add aether -- aether mcp ``` For a session that needs to issue writes (device control, rule changes) — read [Safe Operations for Applications and Agents](/en/guides/safe-operations/) before doing this against real hardware: ```bash claude mcp add aether -- aether mcp --allow-write ``` **Success criterion:** the client's `tools/list` response includes `channels_list`. The default server exposes only read tools. `--allow-write` registers the current governed write allowlist, but the flag is not command confirmation: every invocation still needs `confirmed: true`, the signed `AETHER_ACCESS_TOKEN` is sent as a Bearer credential, and the bridge adds a request ID. Never automatically retry an incomplete write response. For a channel mutation, preserve `request_id`, `resulting_revision`, and `reconciliation_required`; successful desired-state commit can still report a degraded runtime projection. See [Connect AI Assistants](/en/guides/ai-assistants/) for Claude Desktop config and pointing at a remote installation. Now ask the assistant: ```text Get started with AetherEdge. Inspect the runtime in read-only mode and explain which application capabilities are available before proposing any changes. ``` --- # Product version compatibility This matrix distinguishes released compatibility evidence from planned product combinations. A green local test never upgrades an experimental public contract to production status. ## Current tested baseline | AetherEdge | AetherContracts | AetherCloud | Status | Evidence | | --- | --- | --- | --- | --- | | `v0.5.0` plus the alpha.3 consumer change | `v0.1.0-alpha.3` | Unreleased alpha.3 consumer change | Experimental integration baseline | Identical complete-consumer locks, 53 exact imports, no pending imports, and 25 shared fixture outcomes | | `v0.5.0` legacy MQTT path | Not required for legacy wire | Existing legacy ingestion | Compatibility default | Existing product behavior; CloudLink does not silently reinterpret legacy topics | | Future AetherEdge release | Future production contract release | Future production CloudLink release | Planned | Requires joint authentication, signed acknowledgement, crash durability, conformance, rollback, and elapsed support-window evidence | The first row is distribution and fixture evidence. It is not production transport, authentication, state-machine, or durability conformance. ## Naming compatibility | Surface | Migration behavior | | --- | --- | | GitHub repository | `EvanL1/AetherIot` becomes `EvanL1/AetherEdge`; consumers should update remotes even while GitHub redirects old URLs | | Rust SDK package | `aether-edge-sdk` remains unchanged | | Rust import | `aether_sdk` remains unchanged | | CLI and service binaries | `aether` and `aether-*` remain unchanged | | Installer | `AetherEdge--.run` remains unchanged | | Configuration and environment keys | Existing `aether` and `AETHER_*` identifiers remain unchanged | | CloudLink and contract identifiers | Remain unchanged | | AetherContracts alpha.3 artifacts | Remain byte-for-byte immutable and may retain the historical AetherIot name | ## Release rule Every future product release should publish a compatibility row that pins exact versions or commits and links to its executable evidence. Floating branches, `latest`, and implied compatibility are not accepted evidence. --- # System Architecture Aether is an AI-native industrial edge gateway built as six independently supervised Rust services around a shared-memory hot path. Devices are polled by aether-io, values land in SHM, and each real-time consumer resolves its logical points from SQLite and reads the segment directly. Optional extensions may mirror SHM into an external store, but no default service reads that mirror. Generated applications and downstream product interfaces are optional clients; they are not architecture boundaries of the edge kernel. ``` Devices ─────► aether-io(:6001) ───── authoritative SHM live state protocols sole T/S writer │ │ ▲ │ └─ optional Redis StateMirror │ SHM + UDS │ └──── aether-automation(:6002) (rules / C/A command owner) │ ┌──────────────┬──────────┼──────────────┐ ▼ ▼ ▼ ▼ aether-alarm(:6007) aether-history(:6004) aether-api(:6005) aether-uplink(:6006) SHM + own event SHM sampling SHM + own event SHM sampling bitmap / UDS SQLite history bitmap / UDS durable outbox │ │ │ │ └─ local HTTP ─┘ └─ WebSocket └─ MQTT cloud SQLite aether.db ───── configuration/discovery for every process SQLite history.db ──── default local historian store PostgreSQL/TimescaleDB ─ optional history adapter ``` In the reference Docker deployment (`docker-compose.yml`) every container runs with `network_mode: host`. `/dev/shm` is mounted read/write at `/shm/rtdb` for the main segments, per-consumer subscription bitmaps, and cross-container UDS sockets. The five internal process APIs bind to `127.0.0.1`; only the JWT-protected `aether-api` gateway is remotely reachable. Device actions are also authenticated again by automation, so loopback headers cannot impersonate an operator. No core service mounts a Redis socket or waits for an external database. ## Services Default ports are defined once in `libs/aether-model/src/service_ports.rs` and used as fallbacks when configuration does not override them. | Service | Port | Role | |---------|------|------| | aether-io | 6001 | Communication service — industrial protocol drivers (14 protocols: Modbus, IEC 104, IEC 61850, OPC UA, MQTT, HTTP, DL/T 645, CAN/J1939, GPIO, BLE, Zigbee, Matter, Aether-485, Virtual), channel management, sole writer of telemetry into shared memory | | aether-automation | 6002 | Model service — product definitions, device instances, rule engine execution | | aether-history | 6004 | Historical data service — embedded SQLite by default; optional PostgreSQL / TimescaleDB via `postgres-storage` | | aether-api | 6005 | API gateway — unified REST API, WebSocket push to browsers, JWT authentication | | aether-uplink | 6006 | Network service — MQTT broker integration for the cloud uplink, TLS certificate management | | aether-alarm | 6007 | Alarm service — alarm rules, alarm events, notifications | | aether-redis | 6379 | Optional infrastructure for the separately built Redis `StateMirror` extension (`redis` profile) | | TimescaleDB | 5432 | Optional time-series database for historical data, runtime-configured through aether-history | ## Optional Data Processing application Aether Data Processing adds an industry-neutral application capability without changing the six-service default. It assembles bounded observation frames from read-only live state, history queries, request context, and industry-pack bindings, then invokes a configured local or remote `DataProcessor`. ```text authenticated HTTP │ typed processing query (non-idempotent) ▼ DataProcessingApplication ├─ LiveState (read-only) ├─ HistoryQuery └─ task/context inputs │ complete ProcessingFrame ▼ DataProcessor │ validated, expiring result ▼ direct DerivedData response ``` The processor is deliberately outside every Aether data authority: it cannot attach to SHM, read the history database, or resolve a `plant_id` by calling back into internal service APIs. No processor is required by the default runtime. A deployment may compose the capability in-process or isolate model and network dependencies behind a processor sidecar. Version 1 hosts `DataProcessingApplication` in opt-in `aether-api`; no standalone `aether-data-processor`, cache, CLI/MCP binding, or scheduler is implemented. The process name remains reserved for a future orchestration boundary. The default SQLite read is one invocation-time snapshot, not a bitemporal historical cut. `as_of` filters event time, while late ingestion, physical source epochs, and model training/availability cuts require frozen evaluation inputs or stronger adapters/contracts. Data Processing never writes the IO-owned T/S plane and never dispatches a device command. Automation may consume fresh, validated derived data as one input to a separate planning or control use case, whose authorization, safety, and audit rules remain unchanged. See [Data Processing](/en/concepts/data-processing) and [Data Processing Flow](/en/concepts/data-processing-flow). ## Communication paths Latency figures below come from historical `README.md` and CHANGELOG measurements on production hardware (Cortex-A55 @ 1.4 GHz, ECU-1170). Release qualification uses the current cross-process stress and soak gates. | Path | Mechanism | Latency class | |------|-----------|---------------| | aether-io → all consumers (live data) | Shared-memory write; each consumer resolves configured slots from SQLite | ~10 ns per point into SHM | | aether-io → aether-automation/aether-alarm/aether-api (point-change hints) | Independently filtered PointWatch bitmap + UDS per consumer | bounded, sub-millisecond local event path; polling repairs drops | | aether-automation → aether-io (control commands) | Shared-memory write plus UDS notification (`ShmCommandListener` on the aether-io side) | sub-millisecond; ~215 µs P50 including rule evaluation (measured) | | aether-io → device (protocol write) | Field bus (Modbus, IEC 104, etc.) | +5–10 ms; dominates the physical control loop | | aether-alarm → aether-api, aether-uplink | HTTP (targets configured via `AETHER_API_URL` / `AETHER_UPLINK_URL`) | local HTTP | | aether-uplink → cloud | Legacy MQTT by default; experimental broker-neutral CloudLink MQTT v1 is opt-in | network | | aether-api → generated/downstream clients | Authenticated HTTP and WebSocket | network | | all services ↔ SQLite | In-process configuration discovery (`AETHER_DB_PATH`); aether-history uses a separate embedded history file | local | The UDS notification channel reconnects automatically with exponential backoff (1–5 s) if aether-io restarts, so an aether-io restart does not require restarting aether-automation. Two properties keep the hot path safe: - **Write ownership.** aether-io is the only writer of telemetry/signal slots in shared memory; aether-automation is the only writer of control/action slots. See [Shared Memory](/en/concepts/shared-memory). - **Events are hints, SHM is truth.** Event consumers always re-read the slot; aether-history and aether-uplink retain interval-based sampling semantics. - **External stores are extension-only.** All six default services start and operate without Redis or PostgreSQL. A mirror or history adapter may be enabled independently without becoming part of the control path. ## Startup order aether-io owns point/health SHM publication and normally starts first. It publishes both planes with one non-zero epoch and a final commit witness; aether-automation can only attach to files that match its SQLite-derived manifests and the same committed publication. The ordering is enforced in application code, not by making every peripheral service depend on Redis. Peripheral SHM readers open lazily and can start before aether-io; a missing writer is a retryable read-time condition. On an aether-io restart, new point and health generations are fully initialized and atomically renamed over their canonical paths. Existing consumers keep the old inode until their periodic identity check reopens the new generation, and their subscription bitmaps are not truncated: 1. During startup, aether-automation calls `common::dependency::wait_for_dependency("aether-io", /health, 30s)` (`services/automation/src/bootstrap.rs`). The helper (`libs/common/src/dependency.rs`) polls the health URL every 2 seconds until it returns HTTP 2xx or the timeout expires. If aether-io is still not healthy after 30 seconds, aether-automation logs a warning and continues starting — with shared memory possibly unavailable until aether-io comes up. 2. When aether-automation opens live state, `ShmReadTopologyGeneration` checks both physical headers and the commit witness. A hash, slot count, epoch, or writer-generation mismatch remains retryably unavailable until the service publishes one complete generation. ## Configuration flow ``` config/*.yaml ──► aether sync ──► SQLite (aether.db) ──► services load at startup ``` Configuration is authored as YAML (and CSV point tables) under `config/`. The `aether` CLI parses it and writes it into the shared SQLite database (`tools/aether/src/core/syncer.rs`); services read only from SQLite — no service crate parses YAML. Every service container receives the same `AETHER_DB_PATH` pointing at `aether.db`. aether-io automatically reconciles channel runtime, point/health layout, protocol mappings, and routing projections from SQLite; governed explicit reconciliation remains available for operator recovery. ## Where state lives - **Live point values** — the shared-memory segment (`AETHER_SHM_PATH`, `/dev/shm` on Linux). This is the source of truth for the hot path; see [Shared Memory](/en/concepts/shared-memory). - **Optional mirrors** — extensions such as `aether-redis-bridge` can observe SHM and publish an eventually consistent external view. They are never a source of truth and are not startup dependencies of core services. - **SQLite (`aether.db`)** — all configuration: channels, products, instances, rules, service settings. Written only by `aether sync` and the services' own config APIs. - **History database** — embedded `aether-history.db` by default. PostgreSQL / TimescaleDB remain opt-in adapters for larger deployments. ## Related pages - [Shared Memory](/en/concepts/shared-memory) — segment layout, seqlock, write ownership - [Data Flow](/en/concepts/data-flow) — upstream and downstream paths end to end - [Data Processing](/en/concepts/data-processing) — optional cross-industry processing orchestration - [Data Processing Flow](/en/concepts/data-processing-flow) — data assembly and derived-result flow - [Rule Engine](/en/concepts/rule-engine) — how aether-automation evaluates and executes rules - [Data Model](/en/concepts/data-model) — products, instances, points - [Deployment Guide](/en/guides/deployment) — Docker Compose and installer --- # Data Flow Aether moves data along two independent paths. The **uplink** carries measurement points — telemetry (T) and signal (S) values — from devices through aether-io into shared memory, and from there to every consumer. The **downlink** carries action points — control (C) and adjustment (A) commands — from the rule engine or the HTTP API through aether-automation back to a device. Live point values and command transport use the shared-memory segment as the source of truth and transport. No default service needs Redis or PostgreSQL for live data. ## Uplink (device → consumers) 1. A protocol frame arrives on a communication channel and the channel's protocol adapter in aether-io decodes it into point values. 2. aether-io commits each typed T/S batch through `ShmAcquisitionStateWriter` (`extensions/shm-bridge/src/acquisition_writer.rs`). The adapter validates the immutable manifest and writer generation before and after mutation; slot-indexed writes are private implementation detail. 3. **Event path (immediate).** After every slot write, the `PointWatchPublisher` (`extensions/shm-bridge/src/point_watch.rs`) checks the independent bitmap owned by each event consumer. On a hit, a bounded queue sends a `PointWatchEvent` to that consumer's UDS. aether-automation, aether-alarm, and aether-api cannot steal or overwrite one another's subscriptions. The event is a wake-up hint only; each consumer re-reads SHM, and polling repairs dropped events. 4. **Direct read path.** Consumers resolve channel/instance coordinates from one SQLite topology snapshot and re-read matching SHM slots. History and Uplink bind their exact configured points and needed routes to one committed point/health epoch, then pin that immutable generation for a whole collection/upload pass. Events do not silently change their cadence. The production `aether-uplink` composition still uses deprecated legacy MQTT topics and its generic `FileOutbox` delivery boundary. The experimental CloudLink path converts the same pinned generation into `PointSample` facts, adds publication epoch plus topology digest, and places canonical business content in a separate `CloudLinkSpool`. MQTT QoS 1/PUBACK advances only the transport state; a matching cloud durable application ACK is the sole removal authority. Disconnect keeps records replayable under the same stream position/batch ID/digest and does not block any SHM consumer. ``` Device ──frame──► aether-io protocol adapter (decode) │ ▼ set_direct (~10 ns/point) SHM T/S slot (authoritative) │ │ per-consumer │ │ periodic sampling bitmap + UDS │ ├─► aether-history ┌───────────┴────┐ └─► aether-uplink ▼ ▼ aether-automation aether-alarm/aether-api event hint event hint → SHM re-read ``` ## Downlink (rule/API → device) 1. An external HTTP, CLI, or MCP control call becomes a transport-neutral `RequestContext` in aether-automation. `ControlApplication` checks the `device.control` permission and explicit confirmation, persists a mandatory attempted audit event in local SQLite, and only then calls the command dispatcher. An internal deterministic rule action enters the existing dispatcher path directly during the staged migration. 2. The dispatcher calls aether-automation's `execute_action` (`services/automation/src/instance_data.rs`), which resolves the instance action point to its channel command point **once**, from the in-memory routing cache (a mirror of the `route:m2c` table populated by `aether sync`). The resolved target is threaded through the rest of the call so a concurrent routing reload cannot change the decision mid-flight. 3. The offline gate reads the channel-health SHM segment. An offline channel rejects the write with `ChannelUnreachable` before anything is written. 4. After value validation, `ShmDeviceCommandSink` (`extensions/shm-bridge/src/command_sink.rs`) mirrors the C or A slot. The writer generation and canonical path are checked before and after the write; a mismatch means aether-io restarted and rebuilt the segment, so the write is discarded and the dispatch fails rather than landing in a stale layout. 5. The same command adapter sends a fixed-size 56-byte frame over a Unix domain socket. The notification carries the channel/point coordinates, the value bits, issue/expiry timestamps, and a producer id + sequence number for deduplication. If aether-io is down, the notifier reconnects with exponential backoff (1–5 s). Native deployments default to `/tmp/aether-m2c.sock`; Docker sets `AETHER_M2C_SOCKET` to `/shm/rtdb/aether-m2c.sock` so both isolated containers see the socket. 6. aether-io's `ShmCommandListener` (`services/io/src/core/channels/shm_listener.rs`) receives the notification, rejects expired frames, deduplicates by sequence, and forwards a command to the owning channel's queue. Immediately before protocol dispatch, `CommandGuard` verifies that the writable point exists and that the value satisfies its min/max/step policy; only then can the protocol adapter write it to the field bus. Live command data never transits a database: the transport is SHM plus the UDS notification. Local SQLite stores security audit events around external commands, but is not part of command delivery and never mirrors the live point value. A dispatch that fails partway (shared memory written but the notification lost, or no writer available) surfaces as an error to the caller; see [Data Model](/en/concepts/data-model) for how those failures map to HTTP statuses. ## Data-processing path (source data → derived data) Aether Data Processing introduces a third, non-authoritative path for request-driven computation. It is neither an uplink mirror nor a downlink command path: ```text caller │ typed data-processing task ▼ DataProcessingApplication ├─ HistoryQuery ───────────── historical observations ├─ LiveState ──────────────── current read-only tail └─ task/request context ───── future or external covariates │ ▼ complete, bounded ProcessingFrame DataProcessor │ ▼ schema-validated, expiring ProcessingResult authenticated HTTP DerivedData response ``` The application resolves semantic bindings, aligns and aggregates timestamps, requires commissioned unit/sign metadata to match exactly, checks missing and stale inputs, and sends the values in the processor request. Version 1 performs no runtime unit/sign conversion. The processor never receives credentials for SHM, SQLite, or internal service APIs and never resolves a site identifier by reaching back into Aether. The result records its input watermark, input digest, processor provenance, quality, status, and expiry. It is derived evidence rather than a measurement: it is not written to the IO-owned T/S segment. If automation uses the result, a separate planning/control use case validates freshness and safety before the existing audited command path can act. Processor loss therefore removes an optional advisory input without interrupting acquisition or local safety rules. See [Data Processing Flow](/en/concepts/data-processing-flow) for the complete contract. An event-time `as_of` is not by itself a historical knowledge cut. The current historian has no ingestion/source epoch and artifact provenance has no training/availability cut, so point-in-time evaluation uses frozen history and artifact inputs rather than querying today's mutable sources for an old frame. ## Latency budget The microsecond figures are historical measurements on production hardware (Cortex-A55 @ 1.4 GHz, ECU-1170 / EdgeLinux 22.04) recorded in the README and CHANGELOG. The nanosecond figure is the README's stated order of magnitude for the hot-path write; release qualification must rerun current stress gates. | Stage | Latency | Source label | |-------|---------|--------------| | aether-io shared-memory write (`set_direct`) | ~10 ns/point | README | | Data change → aether-automation event received (PointWatch delivery) | P50 206 µs, P99 526 µs | README/CHANGELOG, measured | | + rule evaluation + control SHM write + UDS notify to aether-io | ~215 µs P50, ~540 µs P99 (cumulative) | README, measured | | + device protocol write (Modbus / IEC 104 field bus) | +5–10 ms | README | | aether-alarm → aether-api/aether-uplink, service HTTP hops | local HTTP | — | The CHANGELOG also records P99.9 at 1.4–2.2 ms for the event path, and notes that PointWatch replaced the previous 100 ms Redis-tick polling model (50–150 ms end to end) — roughly a 500× improvement on the critical path. The software-internal control path is sub-millisecond; the field-bus write dominates the physical control loop. ## Optional state mirrors External state mirrors are extensions, not participants in the control path. `extensions/redis-bridge` implements the `StateMirror` extension contract and is built and started explicitly. It may observe SHM and publish an eventually consistent remote view, but no default service reads from it and mirror failure cannot affect acquisition, rules, alarms, history, API reads, uplink, or command delivery. The same boundary applies to other custom stores: consume SHM/events through the extension API, keep the store non-authoritative, and do not add the store to core service startup dependencies. ## Related pages - [Architecture](/en/concepts/architecture) — the services these paths connect - [Shared Memory](/en/concepts/shared-memory) — slot layout, seqlock, write ownership - [Data Model](/en/concepts/data-model) — points, instances, and NaN/absence semantics - [Data Processing](/en/concepts/data-processing) — the optional industry-neutral processing boundary - [Data Processing Flow](/en/concepts/data-processing-flow) — processor-request data flow and failure semantics - [CloudLink MQTT v1](/en/reference/cloudlink-mqtt-v1) — experimental application-ACK/replay edge path - [Rule Engine](/en/concepts/rule-engine) — what happens after a PointWatch event arrives --- # Data Model Aether models the physical plant in three layers: products (device-type templates), instances (individual devices), and points (single measurable or actionable quantities). The central design invariant is that an instance is a **pure thing-model**: it holds logical structure plus current values, and nothing else. Connectivity, alarms, and routing live in separate datasets that reference the instance without ever being copied onto it. ## Three layers **Product** — a template describing what points a device *type* has. Defined in `Product` (`libs/aether-config/src/automation.rs`): a unique `product_name`, an optional `parent_name` for the type hierarchy (Station → ESS → Battery/PCS, and so on), plus three point lists: - `measurements` — measurement point definitions (id, name, unit, description) - `actions` — action point definitions (id, name, unit, description) - `properties` — property templates (static configuration values, e.g. rated power) The kernel product library is empty by default. Validated active Packs provide JSON model assets (the Energy Pack owns its models under `packs/energy/models/`), where the same three lists appear as `M`, `A`, and `P` and the hierarchy as `name` / `pName`. A site may add an explicit custom directory. Product candidates pass the same confinement, regular-file, size, JSON, and duplicate-name checks in validation and at runtime. **Instance** — one physical device. Defined in `Instance` / `InstanceCore` (`libs/aether-config/src/automation.rs`): - `instance_id` (numeric, unique) and `instance_name` - `product_name` — which template this device instantiates - `parent_id` — position in the site topology (None for root instances) - `properties` — concrete values filled in for the product's property templates - optional `created_at` — a creation timestamp (a bookkeeping fact about the record itself, not aggregated runtime state) That is the complete field list. There is no `status`, `health`, `online`, `degraded`, or `alarm_state` field — see [the purity rule](#the-purity-rule). **Point** — a single measurable or actionable quantity, identified by a numeric id that is unique within its product and its point list. A point on the acquisition side is something the device reports (state of charge, breaker position); a point on the command side is something the system can set (start command, power setpoint). ## Point types Channel-side points use the four-type classification defined by `PointType` in `libs/aether-core/src/types.rs` (re-exported through `aether-model`). The enum documentation calls this the standard IEC "Four Remote" classification; each variant carries a serde alias for the Chinese-standard code (YC/YX/YK/YT). | Type | Name | Signal kind | Direction | Write owner | |------|------|-------------|-----------|-------------| | `T` | Telemetry | Analog measurement (YC) | device → system | io | | `S` | Signal | Digital status (YX) | device → system | io | | `C` | Control | Digital command (YK) | system → device | automation | | `A` | Adjustment | Analog setpoint (YT) | system → device | automation | `PointType` provides the category predicates the codebase uses everywhere: `is_measurement()` is true for T and S, `is_action()` for C and A, `is_analog()` for T and A, `is_digital()` for S and C. Write ownership is enforced by construction at the port and composition boundaries. IO alone receives `LiveStateWriter` and publishes T/S acquisition batches through `ShmAcquisitionStateWriter`. Automation submits typed C/A commands through `ShmDeviceCommandSink`; it cannot obtain the acquisition writer, and the sink rejects non-command point kinds before touching SHM. On the instance side, the four channel types collapse into two roles, defined by `PointRole` in `libs/aether-model/src/types.rs`: - `M` (Measurement) — data flows device → model - `A` (Action) — data flows model → device Live values are addressed by typed SHM coordinates. Channel-to-model and model-to-channel mappings are durable configuration in SQLite and are loaded into in-process routing indexes. A custom mirror may choose its own external key shapes, but those keys are not part of the core data model. ## The purity rule An instance holds **logical structure plus current values, and nothing else**. The `Instance` struct has identity, hierarchy, properties, and point mappings. No aggregated state of any kind — no `status`, no `health`, no `online`, no `alarm_state` — exists on it. This is a deliberate design decision, not an omission. Each candidate status field is actually a property of a *different* subsystem with its own writer and its own lifecycle: - "Online" is a property of a **communication channel**, not a device. An instance's points bind to channel points through the routing tables, and nothing forces them all onto one channel. An instance-level online flag would be a lossy aggregate of per-channel facts that io publishes in the channel-health SHM segment. - "Has an active alarm" is a property of the **alarm event stream**, which alarm owns in its own tables. Alarms reference points by coordinates; the reference points one way only. - "Last control write failed" is a property of a **single call**, and it surfaces in that call's return value (see [Consequences for UIs and agents](#consequences-for-uis-and-agents)). Copying any of these onto the instance would create a second copy of a fact whose authoritative writer lives elsewhere. Second copies go stale, conflict with the original under partial failure, and force every writer to know about every consumer. Keeping the instance pure means each fact has exactly one writer and consumers join the datasets they need at read time. ## Four orthogonal datasets The runtime state of the system is split across four datasets. Each has one writer; none is derived from or copied into another. | Dataset | Where | Meaning | Writer | |---------|-------|---------|--------| | Instance current values | Typed point slots in live-state SHM, resolved through the routing index | Thing-model values (a point may be NaN if never acquired) | io (`M` source values); automation (`A`, on action execution) | | Channel connectivity | Channel-health SHM segment | Per-channel online/offline state and heartbeat | io | | Alarm events | alarm SQLite tables `alert` and `alert_event` (`services/alarm/src/db.rs`) | Event stream: trigger/recovery rows addressing points by `service_type` + `channel_id` + `data_type` + `point_id` (for instance points, `service_type` is `"inst"` and the id column holds the instance id) | alarm | | Routing configuration | SQLite mappings loaded into per-process indexes | Static point-to-point wiring between channel points and instance points | Configuration sync/API transaction | The routing indexes are the join keys between the others: C2M maps a channel point (`{channel_id}:{T|S}:{point_id}`) to an instance measurement (`{instance_id}:M:{point_id}`), and M2C maps an instance action (`{instance_id}:A:{point_id}`) back to a channel command point. Alarm rules address values by the same coordinates. Instances never reference alarms, connectivity, or routing back — the arrows point one way. ## NaN as a sentinel In the shared-memory plane, "no data has ever been written here" is encoded in the value itself. Every SHM `PointSlot` (`crates/aether-dataplane/src/core/slot.rs`) is created with both its value and raw-value fields set to a quiet IEEE-754 NaN (the hardcoded bit pattern `SLOT_UNWRITTEN_BITS = 0x7FF8_0000_0000_0000`). The first real write replaces the sentinel with a finite double. This removes the historical ambiguity where a zero-initialized slot was indistinguishable from a genuine reading of 0.0. There is **no side-channel quality flag** in the cross-service data plane. The 32-byte `PointSlot` layout is value, timestamp, raw value, seqlock sequence, and dirty flag — nothing else. (io's protocol layer does track per-point quality codes internally in `services/io/src/protocols/core/`, but they never cross the SHM boundary.) The value is the data; consumers must check for NaN explicitly: - SHM readers probe `PointSlot::is_unwritten()` or `f64::is_nan()` on the returned value. - The rule engine (`libs/aether-rules/src/executor.rs`) tracks which variables were unavailable in `RuleReadOutcome` and skips evaluation rather than substituting 0.0 — otherwise a condition like `current < threshold` would silently fire on missing data. The rule for every consumer is the same: absence of a valid finite value is a first-class outcome you must handle, not an error state recorded somewhere else. ## Consequences for UIs and agents **Graying out a control button is the client's join.** To decide whether a control on instance 7 can currently reach its device, resolve the instance's action point through the M2C routing index to a channel id, then read that channel from the channel-health SHM segment. The backend deliberately does not pre-join this onto the instance. automation performs the same join at write time before dispatching. **Control-write failures surface in the caller's return value, never as instance state.** When the target channel is offline, automation rejects the write with `AutomationError::ChannelUnreachable` (`services/automation/src/error.rs`); a degraded dispatch path (SHM written but the notification to io failed) fails with `AutomationError::DispatchDegraded`. Both reach HTTP through `AetherErrorTrait::http_status` (`libs/errors/src/lib.rs`), where their categories — `ResourceBusy` and `Network` respectively — both map to **HTTP 503**; callers distinguish them by error code (`AUTOMATION_CHANNEL_UNREACHABLE` vs `AUTOMATION_DISPATCH_DEGRADED`), and both are retryable. In the rule engine, an action that cannot be resolved produces an `ActionResult` with `success: false` and a NaN value via the executor's `action_skipped` path, attributed to the variable that caused the skip. In every case the failure is information for *that caller, that call* — it is reported, logged, and possibly retried, but nothing is written back onto the instance. The next reader of `inst:{id}:A` sees the last accepted command, not a failure flag. ## Related pages - [System Architecture](/en/concepts/architecture) — services and how they communicate - [Shared Memory](/en/concepts/shared-memory) — the SHM plane in depth: slots, seqlock, writer ownership - [Data Flow](/en/concepts/data-flow) — the uplink and downlink paths end to end - [Product Models](https://github.com/EvanL1/AetherEdge/blob/main/docs/domain/product-models.md) — the product library and its domain meaning - [Safe Operations](/en/guides/safe-operations) — why control writes are gated and how failures propagate --- # Data Processing Flow Aether uses a request-driven data-processing path. The `DataProcessingApplication` assembles a complete, time-bounded `ProcessingFrame` and sends it to a selected `DataProcessor`. A processor never receives a site identifier and then calls back to SHM, history, or configuration to discover its inputs. **Data requests the processor; the processor does not request Aether data.** This path starts after device data has been decoded and published. It remains outside the SHM write path, historical persistence, rule and alarm hot paths, and command delivery. Version 1 serves an authenticated HTTP query; a future scheduled planning cycle must use the same boundary. Its failure cannot stop device polling, live-state publication, deterministic protection, alarms, or existing control behavior. ## End-to-end path ``` Caller: authenticated HTTP / in-process application work │ ▼ DataProcessingApplication │ resolve task, binding, │ processor policy, as_of │ ┌────────┼───────────────┐ │ │ │ ▼ ▼ ▼ HistoryQuery LiveState CovariateSource historical current SHM weather / calendar / observations Last-only schedule / tariff live tail │ │ │ └────────┼───────────────┘ ▼ ProcessingFrame assembler align / aggregate / validate / quality-policy / digest │ ▼ complete, bounded request DataProcessor local algorithm / model sidecar / remote service │ ▼ ProcessingResult validation │ ▼ authenticated HTTP response │ future separate use case ▼ planner │ constraints and policy ▼ ControlApplication → SHM + UDS → IO ``` The upper path is a query and derived-data path. The lower control path begins only after a separate application decision. `DataProcessor` has no reference to `LiveStateWriter`, an action dispatcher, alarm lifecycle, a planner, or a protocol adapter. ## 1. Start with an explicit task request The caller selects an enabled `DataProcessingTask`, not arbitrary site points, a processor URL, or a model file. A request identifies at least: - the task and task-contract version; - the commissioned binding in which the task runs; - caller/request context and a bounded deadline; - a stable `as_of` timestamp; - task-specific parameters such as forecast horizon; - task-specific typed options. Processor route and artifact policy are resolved from commissioned configuration, never selected by the caller. `as_of` freezes the frame's event-time cut. Historical and live observations newer than the cut are excluded. External forecasts or schedules record the version that was available at that cut. This prevents those sources from crossing their declared time boundary; it is not an API replay guarantee or, with the current historian and artifact metadata, proof of a leakage-safe offline backtest. The application resolves one configuration revision for the complete request: task schema, site binding, unit and sign rules, time policy, quality limits, processor, and egress policy cannot change halfway through frame assembly. Configuration follows the normal Aether path after `aether sync`; a runtime service does not parse an industry pack's YAML directly. ## 2. Query the historical window `HistoryQuery` supplies the task's bounded lookback window for its bound observation fields. It is a domain capability, not a database abstraction. `DataProcessingApplication` and `DataProcessor` see neither SQL nor storage-specific series keys. The default production adapter opens the existing `aether-history.db` lazily in read-only/query-only mode. It does not create or migrate the historian schema. Every feature read in one logical request shares one SQLite transaction and therefore one snapshot. For cadence `c`, history labels end at `as_of`; a label `t` reduces raw observations in `(t-c, t]` using that feature's commissioned aggregation and duplicate policy. Empty buckets stay missing, and the source watermark is the newest numeric raw row that participated rather than `t`. `aether-history` alone owns schema migration, writes, retention, and the file lifecycle. The query adapter depends on SQLite snapshot/WAL semantics and read permissions. If the file is temporarily absent or inaccessible, only the processing request becomes typed-unavailable; its lazy reader may recover on a later call. An external historian is not silently substituted. Production permissions must make that ownership physical: the API receives a dedicated read-only historian database/WAL/SHM directory or read-only identity, separate from its writable configuration/audit database. SQLite read-only flags on the base shared `/app/data:rw` mount do not provide that boundary. The read transaction is a snapshot of the database when the request runs, not of what the database contained at historical `as_of`. Rows lack ingestion time and source/configuration epoch. Consequently, a late correction with an old event timestamp can appear in a later replay, and a physical remap hidden behind the same logical `(series_key, point_id)` can join old and new source epochs. Expected task/binding revisions guard current configuration only; they cannot filter metadata absent from history rows. Use a frozen database/export for offline evaluation, or add a bitemporal, epoch-bearing `HistoryQuery` before claiming point-in-time reproducibility. The runtime's SQLite authority guard compares the route with persisted `history_config.storage_*`. Those settings are saved intent: a `PUT /hisApi/storage` does not reconnect the active writer. Treat any storage change as a maintenance boundary—disable processing, reconnect or restart the historian, validate an expected sentinel series, and restart `aether-api`—so the reader is not joined to a configured path different from the active write path. The optional HTTP history adapter is only for a loopback upstream that already materializes the exact cadence grid. It supports `last/reject`, not raw aggregation. Processors never receive either database or history API access. ## 3. Apply the current live tail Persisted history can lag the current device value by its sampling or flush interval. `DataProcessingApplication` reads only the task's required points through the read-only `LiveState` port and may replace the corresponding final interval cell for explicitly mapped features. A partial live tail changes only those mapped cells; unrelated historical features retain their stored values and provenance. This is valid only when that feature's commissioned history aggregation is `Last`. An instantaneous SHM value cannot represent a `Mean`, `Sum`, `Min`, or `Max` bucket, so v1 rejects `live_tail: true` for those policies. The energy load and PV tasks both use `Mean` for their targets and therefore forbid live tail. SHM remains authoritative for current T/S state. The live read is not a second historian and contributes only the available current samples with their source timestamps. The frame assembler: 1. rejects unwritten, non-finite, future, or over-age live values; 2. maps an accepted sample only to that feature's final interval-end cell; 3. follows the task's explicit source-authority rule for overlaps instead of arbitrary last-writer-wins behavior; 4. records whether the final interval came from history, live state, or both; 5. preserves missingness instead of silently substituting zero. The processor does not map SHM, understand slots or writer generations, or receive `LiveStateWriter`. This keeps the shared-memory ABI inside Aether and preserves IO's exclusive ownership of T/S writes. The current SHM bridge labels accepted finite live values as `good`; it does not preserve device-origin sample quality. Likewise, the current SQLite history schema stores numeric observations without source quality. Version 1 enforces freshness, gaps, missingness, numeric constraints, provenance, and issue time, but a deployment that requires end-to-end device quality must provide a quality-bearing source adapter. ## 4. Resolve covariates and context Some tasks need data that is not a device observation. A forecast may use future weather, calendar fields, tariffs, known setpoints, or a production schedule. A configured `CovariateSource` returns only fields declared by the task. Every covariate carries event time and source provenance. Forecasted covariates also carry an issue time or version so assembly selects the forecast that was available at `as_of`, not a later corrected forecast. This closes the covariate-vintage boundary only; the history and model-artifact cuts have the separate limitations above. Deterministic calendar fields may be generated locally using the task's declared timezone policy. Required and optional fields are explicit. If a required covariate is absent, the frame is unavailable unless the task declares a specific fallback. A stale value is not silently forward-filled merely because a processor accepts a number. ## 5. Assemble the ProcessingFrame The application assembles source-specific samples into one processor-neutral `ProcessingFrame`. A conceptual forecast frame looks like this: ```json { "schema": "aether.processing-frame.v1", "as_of": "2026-07-11T12:00:00Z", "cadence_seconds": 900, "history": { "timestamps": ["2026-07-11T11:45:00Z", "2026-07-11T12:00:00Z"], "features": { "load": { "value_type": "number", "unit": "kW", "values": [820.0, 835.0], "quality": ["good", "good"] } } }, "future_covariates": { "timestamps": ["2026-07-11T12:15:00Z", "2026-07-11T12:30:00Z"], "features": { "temperature": { "value_type": "number", "unit": "Cel", "values": [32.1, 32.0], "quality": ["good", "good"] } } }, "static_features": {}, "quality": { "input_watermark": "2026-07-11T11:59:58Z", "missing_ratio": 0.0, "max_gap_seconds": 900, "live_tail_included": false, "substituted_samples": 0 }, "provenance": [ { "segment": "history", "feature": "load", "source_kind": "history", "source_ref": "energy.site.load.active_power", "watermark": "2026-07-11T11:59:58Z" } ] } ``` The wire codec may be JSON, CBOR, or another versioned representation; the semantic contract stays the same. Named, typed fields keep Aether independent of private algorithm or tensor names and make requests inspectable and usable in offline conformance fixtures. The Aether-side application owns work tied to site-data semantics: - resolving instance and point bindings; - ordering timestamps in UTC and applying declared local-time policy; - validating that commissioned unit, scale, offset, point kind, and target sign already match the task (v1 performs no runtime unit/sign conversion); - applying task-declared aggregation and resampling rules; - aligning fields to a common time grid and preserving missingness masks; - checking lookback, freshness, skew, gap, and completeness requirements; - deriving processor-independent fields declared by the task; - recording source watermarks and provenance. The processor owns work tied to its implementation or artifact: - selecting the configured algorithm or allowed model artifact; - ordering named fields for that implementation; - algorithm-specific transforms, scaling, and tensor construction; - executing a deterministic library, local model, or remote endpoint; - inverse-transforming outputs and reporting implementation provenance. This split keeps device and point semantics out of processors and private algorithm representation out of the Aether kernel. After validation, the application computes a canonical input digest over the versioned task identity, versioned binding identity, processor contract, optional artifact selector, normalized frame (including `as_of`), and typed options. Processor endpoint, request ID, submission time, and deadline are not digest inputs. Independent invocations of the exact same normalized governed content therefore have the same digest; repeating only `as_of` does not ensure that content when sources are mutable. Version 1 does not use the digest for replay or de-duplication. An artifact selector's version and digest identify the bytes used. Version 1 does not carry artifact `trained_through` or `available_at`, so selecting a current pinned model for an old `as_of` may still introduce model-vintage leakage. Historical model evaluation must use an externally frozen artifact registry/cut until that metadata is part of commissioning and validation. ## 6. Invoke the DataProcessor `DataProcessingApplication` sends the complete frame, input digest, request ID, deadline, typed output contract, and processor selection policy through the `DataProcessor` port. An adapter may call: - an in-process deterministic algorithm; - a model endpoint in a sidecar on the same edge host; - a separately supervised local processing service; - an explicitly configured remote processing API. A model endpoint is only one request-driven processor. It receives the named observations and covariates in the request and may own model artifacts, feature ordering, scaling, tensor construction, and execution. It does not receive a `plant_id` and then query InfluxDB, Aether history, SHM, or site configuration. All processor locations implement the same port and receive the same governed frame. Location does not change data authority or grant access to arbitrary Aether state. Calls are bounded by payload, concurrency, and deadline limits. `data_processing.process` is non-idempotent: v1 has no replay store or de-duplication contract, and another call may execute processor work again. A caller retries only a typed retryable failure under its own bounded policy, with a fresh deadline and awareness that earlier work may already have run. For a remote processor, egress policy is part of adapter selection. Only task-declared frame fields and required correlation metadata leave the host; credentials, unrelated points, internal storage identifiers, and control capabilities do not. ## 7. Validate ProcessingResult and DerivedData Transport success does not make processor output trusted. `DataProcessingApplication` validates the common `ProcessingResult` envelope and its task-typed processor output. Only then does Aether stamp `DerivedData` for a consumer. Common checks include: - supported result-contract version; - matching request ID, task ID, binding revision, and input digest; - selected processor and artifact identity, version, and digest; - finite numeric outputs and expected engineering units; - expected output shape, timestamp count, and strictly ordered time axis; - task-specific ranges and consistency constraints; - issue time, processor provenance, quality, and bounded expiry; - explicit `produced`, `fallback`, or `unavailable` status. For a forecast, validation also requires timestamps inside the requested future horizon and correctly ordered confidence bounds or quantiles when present. A processor may return fallback data only when the task allows that named fallback and the result identifies it. An approved persistence forecast can be usable with degraded quality; a zero-filled array produced after an exception cannot be reported as a successful forecast. Invalid output rejects the entire result rather than leaking partially trusted values to consumers. ## 8. Return derived data Version 1 returns the validated `DerivedData` directly from the authenticated `POST /api/v1/data-processing/process` route. It does not implement a result cache, replay store, durable derived-data sink, CLI binding, or MCP tool. Those are possible separate capabilities only after their authority, retention, and side-effect policies are defined. `DerivedData` does not enter IO-owned T/S slots or masquerade as a measured point. Its provenance, quality summary, and expiry remain visible to the HTTP consumer. ## 9. Hand off without crossing subsystem boundaries The following handoffs preserve ownership: Only the authenticated HTTP response is implemented in v1. The other rows are constraints on possible future consumers, not current integrations. | From Data Processing | Consumer | Allowed handoff | Not allowed | |----------------------|----------|-----------------|-------------| | Forecast or estimate | Authenticated HTTP caller | Return typed derived data and provenance | Expose arbitrary source data or processor secrets | | Detection score | Alarm application | Submit a typed observation for alarm policy evaluation | Create, acknowledge, or clear an alarm inside the processor | | Precomputed result snapshot | Future deterministic rule or scheduler | Read validated, unexpired data without network I/O in the hot path | Synchronously call a remote processor during rule execution | | Forecast or other derived data | Future planner/optimizer | Use as one bounded-quality planning input | Treat the result as a device command | | Proposed action from a future planner | `ControlApplication` | Re-authorize, constrain, confirm, audit, and dispatch normally | Bypass control policy because a processor produced the input | Automated control therefore uses a separate sequence: ``` future scheduled planning cycle │ ▼ DataProcessingApplication.process │ validated, unexpired DerivedData ▼ planner / optimizer / deterministic policy │ proposed actions ▼ safety constraints + permission + confirmation + audit │ ▼ ControlApplication │ ▼ existing SHM + UDS command path → aether-io → device ``` When processing is unavailable, a planning cycle either uses an explicitly configured deterministic fallback or skips the cycle. It does not continue an expired plan or replay a stale command when a device reconnects. Acquisition, rules that do not depend on derived data, alarms, and safety behavior continue. AI can explain input watermarks, missingness, processor/artifact versions, fallbacks, and expiry because those facts are in the result contract. It does not gain a route from `DerivedData` to SHM or a device. ## Failure behavior Failures are expected at the edge and have deterministic, observable outcomes. | Failure | Required behavior | |---------|-------------------| | Unknown, disabled, or incompatible task/binding | Reject before reading data or invoking a processor | | History unavailable or watermark too old | Mark the frame unavailable; use only a task-declared fallback that does not invent observations | | SHM unavailable, unwritten, or stale | Do not substitute Redis, a mirror, or zero; continue only if the task explicitly permits history-only input | | Required covariate missing or issued after `as_of` | Reject the frame or use a named covariate fallback with degraded quality | | Excessive gaps, clock skew, or unit/sign mismatch | Reject during frame validation; do not ask the processor to guess | | Processor deadline, disconnect, overload, or circuit open | Return processor unavailable; bound retries and leave acquisition/control unaffected | | Processor contract or artifact mismatch | Reject the complete result and surface the incompatibility | | NaN, infinity, wrong shape, bad timestamps, or invalid task constraints | Reject the complete result as invalid processor output | | Processor uses an approved fallback | Preserve its name and degraded quality so consumers can apply task policy | | Planning cannot obtain usable derived data | Skip the cycle or run an approved deterministic fallback; do not dispatch a stale action | | AI client disconnected | Deterministic edge behavior continues; Data Processing is not in a hard real-time loop | Every failure retains a request ID and machine-readable category. Observability may record source watermarks, missingness, durations, processor health, artifact identity, and validation reason while avoiding raw sensitive frame payloads unless an explicit retention policy permits them. ## Boundary summary ``` raw protocol bytes │ ▼ IO decode and canonical T/S publication │ ▼ SHM live authority ──► history persistence/query │ │ └──────────┬────────────┘ ▼ Aether Data Processing bounded ProcessingFrame → DataProcessor │ ▼ validated, expiring DerivedData │ │ │ ▼ ▼ ▼ query UI alarm policy planner │ ▼ ControlApplication ``` Bulk ETL, warehouses, dashboards, and arbitrary BI queries sit outside this operational edge path. Deterministic rules and alarm lifecycle remain their own applications. Data Processing connects governed observations to bounded derived results; it does not absorb the systems on either side. ## Related pages - [Aether Data Processing](/en/concepts/data-processing) — purpose, task contract, processor boundary, and non-goals - [Data Flow](/en/concepts/data-flow) — authoritative uplink and control paths - [Shared Memory](/en/concepts/shared-memory) — current live-state authority and writer ownership - [System Architecture](/en/concepts/architecture) — runtime services and optional extensions - [Rule Engine](/en/concepts/rule-engine) — deterministic scheduling and execution - [Safe Operations for Applications and Agents](/en/guides/safe-operations) — control safety and authorization policy --- # Aether Data Processing Aether Data Processing is the industry-neutral application capability for turning governed IoT observations into validated derived data. It assembles a bounded input frame from Aether-owned data sources, invokes a local or remote processor, validates the response, and makes the result available through one transport-neutral application boundary. Version 1 exposes authenticated HTTP routes in `aether-api`; CLI and MCP bindings are not implemented yet. Any future transport, scheduler, or automation integration must use the same application boundary. Forecasting is the first data-processing task. A load-forecast model endpoint is not a second data platform and does not own the site's data path: it is one request-driven `DataProcessor` that receives a complete `ProcessingFrame` and returns a `ProcessingResult`. This implemented kernel capability does not make data processing a required seventh process, nor does it require a model, cloud connection, or external database in the default distribution. A composition root may run the application in an existing process or isolate it in an optional, independently supervised process. ## Vocabulary | Concept | Name | |---------|------| | Architecture capability | Aether Data Processing | | Application use case | `DataProcessingApplication` | | Configured unit of work | `DataProcessingTask` | | Processing implementation port | `DataProcessor` | | Governed, time-aligned input | `ProcessingFrame` | | Untrusted processor response | `ProcessingResult` | | Validated Aether result | `DerivedData` | The module is called **Data Processing** rather than Forecasting because future industry packs may need windowed estimation, feature derivation, scoring, or classification as well as forecasts. It is not called Analytics because it is not a reporting or BI surface, and it is not named after a model technology: deterministic algorithms and model endpoints implement the same processor contract. ## What belongs here A data-processing task has all of these properties: - it starts from already decoded, semantically identified Aether data; - it operates on a bounded frame and explicit `as_of` time; - its required fields, units, time policy, quality limits, and output schema are declared before execution; - it is invoked through a request/response application use case; - it produces derived data with input, processor, quality, and expiry provenance; - it has no direct device side effects. Examples include a 24-hour site-load forecast, a remaining-useful-life estimate over a vibration window, a comfort score from building measurements, or a quality classification for a completed production cycle. The first standardized task is `Forecast`; later task kinds require their own typed input and output contracts rather than being hidden inside a universal JSON API. ## Core invariants Every implementation must preserve these properties: 1. **Data requests the processor.** `DataProcessingApplication` assembles and sends the complete input. A processor does not call back into Aether to discover a site, read SHM, open the history database, or fetch arbitrary points. 2. **Data authority does not move.** SHM remains authoritative for current live point state, the history capability remains authoritative for stored observations, and configured covariate sources own their data. Processing only creates `DerivedData`. 3. **Inputs are task-scoped.** A task and commissioned binding determine which fields may be read. A caller or processor cannot turn one processing request into an unrestricted site-data query. 4. **Processors are computational adapters.** They may own algorithm code, model artifacts, feature order, scalers, tensor construction, or a remote API client. They do not own Aether point bindings, live-state writes, alarm lifecycle, planning, or device control. 5. **Failure is explicit.** Missing inputs, stale data, processor errors, invalid output, and approved fallbacks remain distinguishable. A technical failure must never be represented as a normal value such as zero. 6. **Derived data is not measured state.** Results retain their input digest, processor provenance, quality, and expiry. They are never written into IO-owned T/S slots or silently promoted to live-state authority. 7. **The hot path is independent.** Acquisition, deterministic protection, alarms, and essential control continue when a processor or AI client is unavailable. 8. **The standalone profile stays standalone.** Redis, PostgreSQL, a cloud processor, and a model service remain optional extensions, not startup dependencies of the Aether kernel. ## Architecture boundary ``` authenticated HTTP / in-process application work │ ▼ DataProcessingApplication ├─ task + commissioned binding ├─ HistoryQuery ├─ read-only LiveState ├─ CovariateSource └─ frame and quality policy │ ▼ ProcessingFrame │ complete request ▼ DataProcessor local algorithm / model sidecar / remote API │ ▼ untrusted ProcessingResult │ validate and stamp ▼ DerivedData └─ v1 direct query response ``` The capability is a vertical slice through Aether's existing layers: - The domain layer owns stable value types for task identifiers, time ranges, units, data quality, result status, processor provenance, and `DerivedData`. - Ports describe narrow capabilities such as `HistoryQuery`, `CovariateSource`, and `DataProcessor`. They contain no SQL, HTTP, vendor, protocol, or tensor-runtime vocabulary. - The application layer resolves task bindings, assembles and validates a `ProcessingFrame`, invokes a processor, validates its result, and applies authorization, egress, deadline, and audit policy. - Extensions adapt those ports to a local algorithm, an HTTP model sidecar, a remote service, weather data, or an optional derived-data store. - Industry packs declare task semantics and feature roles. Site commissioning binds those roles to concrete instances and points. - Composition roots choose concrete adapters and decide whether processing needs process isolation. Core crates never depend on an industry pack or processor extension. See [Data Processing Flow](/en/concepts/data-processing-flow) for the detailed assembly, request, validation, and failure sequence, including which optional result paths remain future work. ### Implemented history and time semantics The current production composition uses a task-scoped, read-only `SqliteHistoryQuery` over the existing `aether-history.db`. It never creates or migrates the historian schema. All feature reads for one processing request share one SQLite transaction/snapshot. For interval-end cadence `c`, a label `t` aggregates raw observations in `(t-c, t]`; the history grid ends at `as_of`, and future covariates and forecast output begin at `as_of+c`. In production, read-only SQLite connection flags must be backed by OS permissions: expose the historian database/WAL/SHM directory to the API through an independent read-only mount or identity, separate from the API's writable configuration/audit store. The base Compose-wide `/app/data:rw` mount does not meet this direct-history commissioning gate. The optional HTTP history adapter is narrower: it accepts only an upstream already aligned to the exact cadence grid with `aggregation=last` and `duplicate_policy=reject`. It does not aggregate raw history. Here `as_of` is an event-time upper bound, not a bitemporal database cut. The current SQLite rows have no ingestion/system timestamp and no source, binding, or configuration epoch. One transaction makes an invocation internally consistent, but a later backfill at an older `time_ms` can change a replay and a remap behind the same logical series can splice two physical sources. Current task/binding revisions do not repair missing row epochs. Rigorous historical evaluation therefore requires a frozen historian snapshot captured at the evaluation cut, or a future `HistoryQuery` implementation that filters both event time and ingestion/source epoch. The artifact selector likewise records identity, version, and digest, but not `trained_through` or `available_at`. A pinned digest makes an execution identifiable; it does not prove that the artifact existed at a historical `as_of`. Until those availability/training cuts are added, Data Processing can assemble bounded online frames and deterministic goldens, but it does not by itself provide leakage-safe historical model backtesting. ### Current quality limitation The domain and wire contracts carry sample quality, but the current production sources do not preserve device-origin quality end to end. The history schema stores numeric observations without device quality, and the SHM bridge marks accepted finite live values as `good`. Version 1 enforces freshness, gaps, missingness, numeric constraints, provenance, and covariate issue time. A site that requires original device quality must commission a quality-bearing source adapter before enabling processing. ## The task, binding, and processor split A `DataProcessingTask` describes a stable operational question. For a forecast it includes the target semantics, observation and future-covariate fields, lookback, horizon, resolution, engineering units, timezone and sign contract, freshness limits, missing-data policy, and typed output contract. It does not contain a channel address, SHM slot, SQL query, tensor node, processor URL, or secret. An industry pack supplies that semantic task definition. Site commissioning binds its fields to concrete Aether instances and points. A `DataProcessor` separately owns the algorithm-specific representation. This separation allows a device to move to another channel without rebuilding a processor, and a model version to change without teaching Aether about tensor layouts. For example, an energy pack can declare a `site-load-forecast` task whose target is active import power. A commissioned site binds `load` to an instance measurement and `temperature` to a weather measurement. A load-forecast HTTP processor then maps those named columns into the feature order and scaling required by its model. The model endpoint receives the data in the request; it does not receive a `plant_id` and query a second time-series database. The conceptual processor contract is: ``` DataProcessingRequest { request context + deadline, task and output contract, processor selection policy, ProcessingFrame, input digest } │ ▼ DataProcessor.process(...) │ ▼ ProcessingResult { request and input identity, processor/artifact provenance, status + quality + expiry, task-typed processor output } │ validate and Aether-stamp ▼ DerivedData { result and acceptance identity, validated task output + provenance } ``` The frame carries named, typed fields instead of a processor-specific tensor or unstructured payload. The untrusted result envelope provides correlation, provenance, fallback status, and expiry. Aether validates its typed output and then creates `DerivedData`. This keeps the data path inspectable and testable, and lets a pinned frame be reused in offline conformance or golden tests without coupling the kernel to one algorithm runtime. It does not promise API replay. ## Forecast is the first task Forecasting exercises the complete cross-industry processing path: - a historical observation window; - an optional current SHM tail only for features aggregated with `Last`; - future or known covariates such as weather, calendar, tariff, or schedule; - unit/sign compatibility, timezone, alignment, and resampling policy; - a multi-step output with explicit horizon and expiry; - optional quantiles or confidence bounds; - content identity based on the exact assembled frame and governed context, without implying that mutable sources can reconstruct it from `as_of` alone. A forecast result contains ordered future timestamps, target semantics, units, and one or more values or quantiles. The common envelope also identifies the task, input digest, processor and artifact version, issue time, expiry, quality, and whether an approved fallback was used. The same `Forecast` contract can support different packs: | Industry pack | Example forecast | Typical observations and covariates | |---------------|------------------|--------------------------------------| | Energy | Site load, PV generation, or demand | Power history, weather, calendar, tariff | | Manufacturing | Production demand or remaining useful life | Cycle count, vibration, temperature, schedule | | Buildings | Cooling load or occupancy | Zone sensors, weather, calendar, booking schedule | | Agriculture | Irrigation demand or soil moisture | Soil sensors, weather forecast, crop schedule | | Transport | Traffic flow or arrival time | Counts, speed, route schedule, weather | Additional processing task kinds may follow, but each must define typed semantics and conformance tests. A generic `process(json)` escape hatch must not become the public application contract. ## Boundary with neighboring subsystems The word "processing" is intentionally broad, so its limits must be explicit. | Neighbor | Its responsibility | Data Processing boundary | |----------|--------------------|--------------------------| | IO decoding | Read protocol frames, validate wire values, apply channel-point decoding, and publish canonical T/S values | Starts only after a value has an Aether point identity and engineering meaning; never decodes Modbus registers, IEC messages, or raw frames | | SHM dataplane | Authoritative current point values and the existing command transport | Read-only input through `LiveState`; no derived result or processor writes IO-owned T/S slots | | History | Sample, persist, retain, aggregate, and query observations | The application reads `HistoryQuery`; the default adapter opens the existing history SQLite file read-only, while processors never receive database access | | Rule engine | Deterministic event/tick evaluation and action execution | Does not replace rules or synchronously call a network processor in a rule hot path | | Alarm service | Alarm policy, lifecycle, severity, acknowledgement, and notification | May produce a score or detection as `DerivedData`; it does not create or acknowledge an alarm by itself | | Bulk ETL / BI | Large scans, arbitrary joins, warehouses, dashboards, and offline reporting | Handles bounded operational frames for declared tasks, not general data pipelines or analytical warehouses | | Planning / optimization | Turn observations and derived data into a proposed operating plan | Stops at validated derived data; it does not choose or schedule device actions | | Control | Authorize, confirm, audit, route, and dispatch device commands | A later caller must enter `ControlApplication`; processor output grants no control permission | The current v1 composition requires commissioned physical unit, scale, offset, and target sign metadata to match the task exactly. It performs alignment and aggregation but no runtime unit/sign conversion. A future explicit transform would create a task-specific view without changing the authoritative measurement and would require its own tests and provenance. ## Data authority | Data | Authority | Processing treatment | |------|-----------|----------------------| | Current T/S point value | IO-owned SHM | Read through `LiveState`; never written by processing | | Historical point observations | `HistoryQuery` implementation | Read through a port; processors never open the store directly | | External or future covariates | Configured `CovariateSource` | Copied with event-time and issue-time provenance | | Task and point binding | Aether configuration after sync | Resolved before the processor request | | Algorithm/model artifact and private transforms | Selected `DataProcessor` | Returned as processor and artifact provenance | | Processing output | Validated `DerivedData` | Derived, quality-bounded, expiring, and returned directly by the v1 API | | Device action | Existing control application and dispatcher | Created only by a separate decision and control use case | `ProcessingResult` must identify the request, task and binding revision, input digest, processor and artifact version, issue time, expiry, status, and quality. Consumers reject unsupported contract versions, non-finite values, mismatched shapes or units, timestamps outside the task range, expired results, and results that cannot be tied to the submitted frame. ## AI-native governance Data Processing is AI-native because task contracts, allowed inputs, quality, processor provenance, permissions, egress, and failure states are machine-readable—not because an LLM sits in the data path. The v1 authenticated HTTP surface invokes `DataProcessingApplication`; future CLI, MCP, and scheduled interfaces must invoke the same use cases. A side-effect-free processing invocation is a Medium-risk query because its configured route may cross a remote data-egress boundary; deployment policy decides confirmation for that approved route, durable audit is required, and the capability is marked non-idempotent because each call may execute processor work again. Low-risk task and processor-health discovery requires no confirmation or durable audit because it carries no observation values. Changing a task, route, artifact selection, egress policy, or fallback policy is a separate configuration command with its own permission, confirmation, and audit metadata. A processing result never inherits permission to operate a device. Remote processors create an explicit data-egress boundary. The application must allowlist the processor, minimize the request to task-declared fields, enforce payload and deadline limits, avoid unrelated identifiers and secrets, and retain enough provenance to explain what data cut and processor produced a result. ## Non-goals Aether Data Processing is not: - protocol decoding, acquisition, SHM replication, or a second live-state plane; - a time-series historian, feature store, data lake, ETL engine, BI platform, or arbitrary query service; - a training pipeline, experiment tracker, or mandatory model registry; - a replacement for deterministic rules, alarm lifecycle, planning, optimization, or `ControlApplication`; - permission for a processor to discover arbitrary site data or query Aether databases; - a generic tensor transport or untyped plugin API; - a requirement that every deployment run a data-processing process; - part of acquisition or hard real-time safety loops. ## Related pages - [Data Processing Flow](/en/concepts/data-processing-flow) — frame assembly, processor request, validation, caching, and failures - [System Architecture](/en/concepts/architecture) — core services and dependency boundaries - [Data Flow](/en/concepts/data-flow) — authoritative uplink and control paths - [Shared Memory](/en/concepts/shared-memory) — live-state authority and writer ownership - [Rule Engine](/en/concepts/rule-engine) — deterministic automation behavior - [Safe Operations for Applications and Agents](/en/guides/safe-operations) — permission, confirmation, audit, and control policy --- # Rule Engine Aether's rule engine lives in the `aether-rules` library and runs inside automation. It has three parts: a parser that turns the visual editor's flow document into a compact execution topology, a scheduler that decides when each rule runs, and an executor that walks the topology, evaluates conditions, and writes action points. This page covers the engine mechanics; for how to express control strategies as rule flows (with a worked state-of-charge example), see [Control Strategies](https://github.com/EvanL1/AetherEdge/blob/main/docs/domain/control-strategies.md). ## Two columns, one writer Every rule persists in the SQLite `rules` table as two parallel columns: - `flow_json` — the complete Vue Flow editor document: nodes with positions and labels, edges, viewport, metadata. This is what the visual editor loads to repopulate its canvas. - `nodes_json` — the compact execution topology the engine actually runs: a `RuleFlow` with a `start_node` ID and a map of node ID to execution node. The parser (`extract_rule_flow`) keeps only what execution needs and discards positions, labels, and edge styling. The invariant: both columns are always produced together by one function, `aether_rules::flow_column_values()`, which parses the editor document and returns a `FlowColumns` struct holding both serialized strings. The struct uses named fields rather than a tuple so call sites cannot silently swap the two values when binding SQL parameters, and an invalid flow fails as a unit — there is no partial output. No code path serializes either column independently. There are three production call sites, and any new write path to the `rules` table must go through the same function: - `repository::upsert_rule` in `libs/aether-rules/src/repository.rs` (used by rule import) - the `PUT /api/rules/{id}` handler in `services/automation/src/rule_routes.rs` - the config syncer in `tools/aether/src/core/syncer.rs` (`aether sync`) One nuance: `POST /api/rules` creates a metadata-only stub — an empty `{}` topology, a NULL editor document, and `enabled = false`. The flow content always arrives later via PUT, which derives both columns together. Legacy rules imported in compact-only form keep a NULL `flow_json`; their `nodes_json` still comes from the same function. Why this matters: if the columns diverged, the editor would display one logic while the engine executed another — an operator auditing a strategy would be reading a lie. Funneling every write through one producer makes that divergence structurally impossible rather than a code-review concern. ## Scheduling A single scheduler loop (`RuleScheduler::start`) multiplexes two inputs with `tokio::select!`: a periodic tick (100 ms by default, `DEFAULT_TICK_MS`) and, when the PointWatch event plane is wired in, a bounded channel of point-change events. A rule declares one of two trigger types in its `trigger_config` column: - **Interval** (`{"type": "interval", "interval_ms": 1000}`) — the rule is due on any tick where the time since its last execution has reached `interval_ms`. Rules with no `trigger_config` default to a 1000 ms interval (or to their `cooldown_ms` as the period, if set). - **OnChange** — the rule subscribes to specific measurement points (M) or action points (A) via `point_refs` and fires when a subscribed value changes beyond its deadbands. OnChange rules are served by two paths running in parallel. The fast path is event-driven: io publishes a PointWatch event when a subscribed point is written, a dispatcher maps the `(channel, point)` pair to rule IDs, and the scheduler evaluates those rules immediately. The event carries the new value, so the trigger decision needs no read-back from the real-time database or shared memory. The tick path is the fallback: each tick, the scheduler samples all subscribed points in one batch (directly from shared memory when available) and re-evaluates every OnChange rule — this covers multi-point rules and keeps rules firing if the event socket is down. Two deadbands filter noise, combined with AND semantics: - `time_deadband_ms` — a rule-level frequency limit; the rule will not fire again until this much time has passed since its last trigger. - `value_deadband` — either absolute (`|new - last| > threshold`) or percent. Without one, any change between finite values counts. NaN values (Aether's "temporarily unavailable" sentinel) never count as a change; the first finite value after a gap triggers once. After each trigger, the per-point "last value" advances to what the executor actually read during execution, so future comparisons are anchored to the value the rule logic actually saw. Due rules execute concurrently with bounded parallelism (four at a time by default). Independently of triggers, a rule may declare a `cooldown_ms`; the cooldown starts only after an execution that succeeded and performed at least one action, and suppresses re-execution until it elapses. ## Execution The executor walks the compact topology from the start node, following each node's wires: switch nodes evaluate condition branches and select an output wire, change nodes write a value to a point, calculation and period-delta nodes compute derived values, and the end node terminates the path. Input variables are read through the SHM-backed `RuleLiveState`, and the reads are strict: if a variable's data is unavailable this cycle, the evaluation short-circuits rather than substituting a default — a missing reading must never satisfy a condition like `current < threshold` as if it were zero. The same discipline applies to writes: a computed value that is NaN, infinite, or out of range is rejected and the action recorded as failed, never coerced to a number. Writes to action points take the command path: the executor resolves the instance's model-to-channel route, then dispatches through the same `ActionDispatch` used by automation's HTTP control endpoint — a write to the shared-memory command slots (the C/A slots) followed by a Unix domain socket notify to io (see [Shared Memory](/en/concepts/shared-memory)). The dispatcher checks the shared-memory writer generation before and after the write, so a rule firing across a io restart is detected and dropped instead of landing in a stale slot. When the target is unavailable — no shared-memory writer after a io restart, a missing C/A slot, or a degraded notify socket — the action is recorded as failed with a reason and the rule's success flag reflects it. Commands are never queued for later delivery: the next cycle re-evaluates from current values instead of replaying a stale setpoint against a device that has since come back in a different state. ## Results and observability Every execution produces a result record: success flag, the list of actions executed (each with target, point, value, and its own success flag), the execution path as visited node IDs, the matched condition, a snapshot of variable values, and per-node execution details. The scheduler writes this record to local durable and observable surfaces: - to a per-rule log file, so each rule has an independent, greppable history; - to SQLite `rule_history`, which keeps the structured execution result and error for API/WebSocket consumers. Neither rule execution nor result observation requires an external database. ## Hot reload `POST /api/scheduler/reload` calls `RuleScheduler::reload_rules`, which re-reads all enabled rules from SQLite and replaces the scheduler's in-memory rule set wholesale under a write lock — running executions finish against the rule they started with. When the PointWatch handles are wired in (production mode), the reload then rebuilds the event plane from the fresh rule set as a unit: the subscription bitmap that tells io which points to publish events for, and the dispatcher index that maps `(channel, point)` pairs to rule IDs. A newly added or re-targeted OnChange rule starts receiving events immediately, with no service restart. In practice the endpoint is rarely needed: the rule CRUD endpoints (create, update, delete, enable, disable) each trigger the same reload after their database write. Explicit reload matters for out-of-band writes — a bulk import or `aether sync` pushing rule files from configuration — where the scheduler would otherwise not know the table changed. ## Related pages - [Control Strategies as Rules](https://github.com/EvanL1/AetherEdge/blob/main/docs/domain/control-strategies.md) — expressing strategies as rule flows - [Shared Memory](/en/concepts/shared-memory) — the command slots and event plane the engine rides on - [Data Flow](/en/concepts/data-flow) — where rule execution sits in the end-to-end paths - [Data Model](/en/concepts/data-model) — the points and instances rules read and write --- # Shared Memory Live values in Aether do not travel through a broker or a database on the hot path. io (the communication service) and automation (the model/rule service) share an IO-owned point segment plus a separate channel-health segment and exchange fixed-size notifications over Unix domain sockets. A small commit witness proves that both segments came from the same physical topology publication. A device reading lands in shared memory in tens of nanoseconds. This page describes the segment itself and the two socket-based signaling planes built on top of it. For the services around it see [Architecture](/en/concepts/architecture); for what the values mean see [Data Model](/en/concepts/data-model). Source of truth: `crates/aether-dataplane/` (physical header, slots, locking), `extensions/shm-bridge/` (typed manifests, point/health publication and self-healing readers), and `services/io/src/core/channels/shm_listener.rs` (the command listener). The former legacy SHM aggregation crate has been removed after the v4 rolling-compatibility gate passed. ## Layout The segment is a single file: a 64-byte header followed by a fixed-size array of 32-byte point slots (`calculate_file_size` in `crates/aether-dataplane/src/core/header.rs` is exactly `64 + 32 × max_slots`; the default capacity is 100,000 slots). Both struct sizes are compile-time asserted. The file path is resolved by `default_shm_path()` (`crates/aether-dataplane/src/core/config.rs`) in this order: 1. `AETHER_SHM_PATH` environment variable, if set. 2. `/shm/rtdb/aether-rtdb.shm`, if the `/shm/rtdb` directory exists (the Docker deployment mounts a shared tmpfs volume there). 3. `/dev/shm/aether-rtdb.shm` on Linux (RAM-backed tmpfs). 4. `/tmp/aether-rtdb.shm` otherwise (macOS development). The header (`UnifiedHeader`, `#[repr(C, align(64))]`) carries: a magic number, a layout version, `max_slots`, the live `slot_count`, a last-update timestamp, a writer heartbeat, `routing_hash` (a fingerprint of the channel/point layout), `writer_generation` (an incarnation counter), and `publication_epoch` (the common point/health publication identity). All multi-byte fields use native endianness, so readers and writers must run on the same architecture. Each `PointSlot` holds an engineering value (f64 bits), a raw value (f64 bits), a millisecond timestamp, a seqlock sequence counter, and a dirty flag. A slot that has never been written holds a quiet-NaN sentinel in both value fields — an unwritten slot is self-describing, never confusable with a real device reading of zero. Downstream consumers filter on `is_finite`. Slots are addressed by flat index. Each process independently derives the same `(channel_id, point_type, point_id) → slot` mapping from the same immutable `ChannelPointManifest`. Agreement is verified through the manifest `routing_hash`, exact slot count, committed publication epoch, and writer generation. Logical measurement/action routing and protocol register mapping do not participate in the physical slot layout. ## Writer ownership is type-enforced Channel points come in four slot types: telemetry (T) and signal (S) are the measurement side; control (C) and adjustment (A) are the action side. The ownership rule is: - **io acquisition owns T/S slots.** `ShmAcquisitionStateWriter` accepts only typed `AcquiredPointSample` batches and rejects C/A addresses before any mutation. - **governed command dispatch mirrors C/A slots.** `ShmDeviceCommandSink` resolves one typed physical target, checks the writer generation before and after the mirror, and sends the complete command frame to io. It cannot write T/S addresses. The protection is primarily typed at the extension port boundary; raw slot-indexed writes stay inside the physical adapter. Runtime checks provide defense in depth for manifest membership, slot bounds, stable generation, and canonical-file identity. `ShmReadTopologyGeneration` provides the production read view. It binds point and health manifests to one commit witness and pins both writer generations; debug tools may still open a single physical segment explicitly. ## Consistency: seqlock Each slot is protected by a per-slot seqlock: the writer bumps the sequence counter to an odd value, writes the three data fields, then bumps it back to even. Readers read the sequence, read the data, and re-read the sequence; the snapshot is valid only if both reads returned the same even value. Memory ordering uses paired Acquire fences on the read side and a Release fence plus Release increment on the write side — the comments in `crates/aether-dataplane/src/core/slot.rs` explain why single Acquire loads are insufficient on AArch64. Two read entry points exist, and choosing the right one matters: - `try_load_consistent()` — a single attempt that returns `None` on any contention (odd sequence or sequence change). This is the variant for tasks running on async runtime worker threads: never spin on a tokio worker. - `load_consistent()` — retries `try_load_consistent` up to 32,768 times with a spin hint, bounding worst-case spinning to roughly 3–16 ms under extreme contention. It is intended for dedicated threads. When retries are exhausted it logs a warning and returns `None` — it never returns torn data. In production the retry path almost never iterates: protocol I/O between writes means a reader rarely collides with a write in progress. ## Generations and rebuilds Three identities let readers detect that their view is stale: - **`routing_hash`** is the fingerprint of the channel point layout. io writes it at create time; every coordinated open path recomputes its own fingerprint from local configuration and refuses to open on mismatch — slot indices would silently point at the wrong points otherwise. The error message tells the operator to restart io to resynchronize. - **`writer_generation`** identifies the writer incarnation. It is seeded at create time from wall-clock nanoseconds combined with a per-process nonce, forced even and nonzero: the invariant is "even at rest, odd while a reconfigure is in flight," so readers gate themselves out on odd values. command/read adapters compare the generation on every operation and detect an io restart or reconfigure it has not caught up with. - **`publication_epoch` + commit witness** bind the point and health files to one completed IO transaction. The witness also records both hashes, counts, and writer generations. Missing, partial, corrupt, or mixed publications fail retryably; readers never guess from equal hashes. Reconfiguration never mutates a live layout in place. `ShmWriterHandle` and `ShmChannelHealthWriterHandle` build complete staging files and atomically rename them over their canonical paths while holding one cross-plane publication lease. The commit witness is renamed last and is the linearization point. Retained mmaps are fenced by an odd writer generation; self-healing readers may reopen only the epoch and writer generation pinned by their service-level topology. History and Uplink replace their SQLite routes and committed SHM read view as one `Arc`, so a collection pass cannot mix logical and physical generations. Crash-orphaned staging files are bounded and cleaned on recovery. ## Command notifications When automation issues a command — a rule action or an HTTP control request (see [Safe Operations for Applications and Agents](/en/guides/safe-operations) for what is allowed to reach devices) — `ShmDeviceCommandSink` mirrors the C/A value into the pinned writer generation and sends a notification over a Unix domain socket (`/tmp/aether-m2c.sock`) so io reacts immediately instead of polling. In measurement the notify path is sub-millisecond; ~1–2 ms is the design budget the dispatch code documents for the happy path. The notification (`DeviceCommandFrame`) is a fixed 56-byte frame carrying the routing target (channel, point type, point), the command payload (value bits plus issue and expiry timestamps), and producer ordering (`producer_id`, a per-incarnation ID that changes on every automation restart, plus a monotonic `seq`). Because the frame carries the full command, io never has to read the slot back — and two rapid writes to the same point arrive as two events rather than collapsing into one. io's `ShmCommandListener` binds the socket, immediately restricts it to mode 0600 (refusing to listen if that fails — anyone who can write this socket can inject device commands), and dedupes incoming events per point: a different `producer_id` always resets state (a automation restart), while within the same producer a frame is dropped as stale or duplicate using wrapping sequence comparison (`seq.wrapping_sub(last_seq) > u64::MAX / 2`). Expired frames are dropped before queueing. The unified channel task then checks the value again against the configured writable point, inclusive min/max, and step immediately before calling the protocol adapter. Unknown points, invalid point constraints, NaN/infinity, and a rejected member of a batch all fail the whole command without touching hardware. On the sending side, `ShmNotifier` retries a failed write three times, then marks itself disconnected and reconnects with exponential backoff (1 s doubling to a 5 s cap). There is no polling fallback: if the socket stays down, the notify result reports degraded delivery and the caller decides what to surface. ## The PointWatch event plane Commands flow automation → io; PointWatch is the reverse direction, and it is what makes the rule engine event-driven (see [Rule Engine](/en/concepts/rule-engine)). After every T/S slot write, io consults a **subscription bitmap** — a separate 12,504-byte mmap file (`aether-rtdb-point-watch-subs.shm`, next to the main segment) of atomic u64 words covering all slots. io creates it zero-filled at startup; automation sets bits when it loads or reloads rules. The hot-path check is a single relaxed atomic load and bit test, about 1–2 ns, and the common case (slot not subscribed) returns immediately. On a hit, io builds a 56-byte `PointWatchEvent` — channel, point, point type, value bits, raw bits, slot index, timestamp, producer ID — and pushes it to a bounded in-process channel (capacity 2048) drained by a background task that batches up to 64 events per write onto a dedicated socket (`/tmp/aether-point-watch-automation.sock`, aether-automation listens, aether-io connects, same 1–5 s reconnect backoff as the command plane). Because the event carries the value itself, automation evaluates deadband directly from the event with no read-back; duplicate events are harmless (at worst an extra deadband check), which is why the frame has no sequence field. On the automation side the pipeline stays bounded end to end: the listener forwards frames into a 1024-capacity channel, and the dispatcher (`PointWatchDispatcher` in `libs/aether-rules/`) maps `(channel, point) → rule IDs` and forwards wake-up events into the scheduler's own 1024-capacity channel. Every stage uses a non-blocking `try_send`; on overflow the event is dropped and a `dropped_count` counter is incremented rather than ever blocking io's write path. Dropped events are recovered by the rule engine's periodic tick, so overload degrades to the old polling latency instead of losing correctness. The payoff, measured on production hardware (Cortex-A55 @ 1.4 GHz, ECU-1170) for the initial PointWatch benchmark: point-change-to-event-delivery latency of 206 µs at P50 and 526 µs at P99 (rule evaluation brings the cumulative figure to ~215 µs P50 — see [Data Flow](/en/concepts/data-flow)), versus 50–150 ms under the previous Redis-tick model — roughly a 500× improvement at the median. ## Related pages - [Architecture](/en/concepts/architecture) — the services that share this segment - [Data Model](/en/concepts/data-model) — what T/S/C/A values mean, and the NaN sentinel - [Data Flow](/en/concepts/data-flow) — uplink/downlink paths and the latency budget - [Rule Engine](/en/concepts/rule-engine) — the consumer of PointWatch events - [Safe Operations for Applications and Agents](/en/guides/safe-operations) — which writes reach devices --- # aether-application Transport-neutral command and query use cases for the Aether edge kernel. `EdgeApplication` is shared by CLI, MCP, and optional network transports. It authorizes point reads, validates device commands, requires an audit sink, and dispatches control through capability ports. Infrastructure choices stay outside this crate, so its default graph contains no Redis, PostgreSQL, SQLx, MQTT client, or web framework. Control, manual rule execution, rule/alarm policy changes, alert resolution, I/O channel commissioning, and physical action-routing mutations persist an `Attempted` audit event before calling a non-idempotent port. Channel audit details identify changed fields but never include protocol parameter or per-channel logging values. If the pre-execution event cannot be stored, execution fails closed. Once the port accepts an operation, failure to append the terminal `Succeeded` event is returned as an `AcceptedOutcome` with `CompletionAuditStatus::Incomplete`, the request and command/rule correlation IDs, and `is_retryable() == false`; it is never turned into a retryable error that could execute the operation twice. Audit details include operation-specific command targets, rule identifiers, action counts, or routing keys. `DataProcessingApplication` is the transport-neutral query facade for Aether Data Processing. A composition root registers a declarative task, a `DataProcessingBinding`, and a `DataProcessor` route. The binding resolves task-local measurement names to read-only `PointAddress` values and may pin static features and an artifact selector; processor routes are never selected by API callers. The landed external binding is the authenticated `/api/v1/data-processing/*` HTTP surface in `aether-api`. Data Processing CLI and MCP bindings are not implemented in version 1. For each processing request the application authorizes before reading data, queries bounded history and covariates, optionally merges an exactly aligned read-only live tail only for `Last`-aggregated features, assembles a complete frame with one provenance entry per feature, computes the shared canonical digest, applies exact frame and payload limits, and treats the processor response as untrusted. Only a correlated, policy-compatible result becomes `DerivedData`. The facade has no SHM writer, history sink, or command dispatcher, and an empty route set remains a valid default configuration. Transport request and result IDs are stable UUIDv5 derivations, while the caller's original request ID remains on audit records. The query is non-idempotent: repeated content can retain stable correlation IDs but still executes the processor and required audit for every invocation. The application bounds source event time; it cannot manufacture chronology that adapters do not provide. With the current SQLite schema and artifact selector, an old `as_of` is not proof of an ingestion-time/source-epoch or model-availability cut. Historical evaluation must supply frozen inputs or ports whose contracts carry and validate those cuts. ```bash cargo test -p aether-application ``` Licensed under either MIT or Apache-2.0, at your option. --- # aether-cloudlink Transport-neutral implementation of the experimental, digest-pinned public AetherContracts CloudLink subset. It provides strict closed JSON decoding, RFC 8785 business digests, session/version/epoch validation, stable delivery envelopes, Runtime Manifest checksum reuse, and truthful `PointSample` mapping. This crate contains no MQTT client and no device-control message. The matching AetherCloud codec consumes the same imported fixtures, while three public behavior artifacts and all production interoperability gates remain open. See the [CloudLink MQTT reference](/en/reference/cloudlink-mqtt-v1) for current behavior and production limits. ```bash cargo test -p aether-cloudlink ``` --- # Aether Data Processing Strict, transport-neutral JSON codec for the Aether Data Processing v1 processor boundary. It converts validated domain values to and from the versioned RFC 3339/JSON DTOs, encodes Aether-accepted `DerivedData`, and computes RFC 8785 input digests. The v1 wire format uses UTC `Z` timestamps with no finer than millisecond precision, preserves optional external-forecast `SourceProvenance.issued_at`, and accepts only `interval_end` forecast timestamp semantics. Contract-invalid or unknown fields fail closed before a transport exposes domain values. `DerivedDataDto` and `encode_derived_data` are intentionally encode-only: an external JSON payload cannot cross the boundary by claiming it was accepted by Aether. The accepted envelope retains the processor contract, immutable artifact provenance, fallback metadata, warning codes, and Aether-computed frame quality. Artifact version/digest is identity, not chronology: v1 has no `trained_through` or `available_at`. The codec also cannot turn an event-time `as_of` into a bitemporal historian cut; those guarantees belong to future source and commissioning contracts. This crate contains no HTTP client, storage access, callbacks, or model runtime. Protocol adapters depend on it; the domain does not depend on any adapter. See the [normative contract reference](/en/reference/data-processing-contracts), [JSON Schemas](https://github.com/EvanL1/AetherEdge/blob/main/contracts/data-processing/README.md), and optional [HTTP adapter](/en/extensions/http-data-processor). ```bash cargo test -p aether-data-processing ``` --- # aether-dataplane `aether-dataplane` is Aether's business-neutral shared-memory core. It can be used by an embedded gateway without Redis, PostgreSQL, SQLx, routing models, or any HTTP stack. It owns: - the stable 64-byte header and 32-byte aligned `PointSlot` layout; - seqlock-consistent reads and single-writer atomic updates; - read-only and writable mmap owners with RAII cleanup; - process-local dirty-slot tracking; - slot allocation bitmaps and generation path helpers; - tear-resistant snapshot serialization. Mmap constructors reject a mapping that cannot cover its declared capacity or whose live slot count exceeds that capacity before exposing any header or slot reference. Public failures use `DataplaneError`, allowing hosts to distinguish invalid layout, invalid path, and operating-system I/O failures. Read-only readers and the generic `SlotIo` trait expose header values as a `HeaderSnapshot`, never as writable atomic cells. Logical manifest validation remains the composition layer's job. ```bash cargo test -p aether-dataplane cargo tree -p aether-dataplane --edges normal ``` The former `aether-rtdb-shm` aggregation crate was retired after its rolling v4 compatibility contracts passed. Industry-neutral code depends on this crate directly; channel-aware composition belongs in `aether-shm-bridge`. --- # aether-domain Industry-neutral, `no_std` domain types for the Aether edge kernel. This crate defines point addresses and samples, strongly typed identifiers, quality states, timestamps, validated control commands, and the Aether Data Processing contract. The processing model separates application-side `ProcessTaskRequest` from a complete processor-side `DataProcessingRequest`, and treats `ProcessingResult` as untrusted until accepted as `DerivedData`. The crate remains `no_std`; owned processing frames use `alloc` collections. It has no async runtime, database, network, service, model framework, or hardware dependency. Use it when implementing an Aether host, extension, protocol adapter, or firmware component that needs to exchange stable edge-domain values. ```bash cargo test -p aether-domain cargo tree -p aether-domain --edges normal ``` Licensed under either MIT or Apache-2.0, at your option. --- # aether-pack Versioned, industry-neutral domain-pack manifest loading for the Aether edge kernel. The crate validates a pack's own release identity, distribution identity, compatible Aether range, required capability and protocol IDs, explicitly uncommissioned examples, and pack-root-confined asset directories. Loading is read-only. It does not install a pack, enable a channel or rule, or commission field hardware. Unsupported or unknown manifest fields, missing requirements, and paths that are absolute, traverse `..`, or resolve outside the pack root fail with typed errors. ```rust use aether_pack::{PackRuntime, load_pack_manifest}; # fn inspect() -> Result<(), aether_pack::PackError> { let runtime = PackRuntime::new("0.5.0") .with_capabilities(["device.read_point"]) .with_protocols(["modbus_tcp"]); let manifest = load_pack_manifest("packs/example", &runtime)?; println!("{} {}", manifest.id(), manifest.version()); # Ok(()) # } ``` ## Active Pack configuration Automation and `aether mcp` consume one shared entry point: `/global.yaml`. The safe default activates no domain Pack: ```yaml packs: [] ``` An operator activates an installed Pack by declaring both the expected identity and its root. The root may be absolute or relative to the configuration directory, but may not contain `..`: ```yaml packs: - id: energy root: /opt/aether/packs/energy ``` The configured identity must match `pack.yaml`. Every selected manifest is validated for Aether compatibility, capabilities, protocols, commissioning, and asset confinement before its models or knowledge become visible. Pack-owned `mappings`, `rules`, `evaluations`, and `data_processing` tasks are formal indexed asset categories. Each directory contains `index.yaml` using `aether.pack.asset-index.v1`; manifest capability IDs, index IDs, and actual regular files must match exactly. Unknown fields/files, duplicate IDs or paths, symlinks, path escapes, media/schema mismatches, and oversized files fail closed. Pack v1 fixes each category to its corresponding v1 payload schema; changing a payload contract requires a Pack contract version change. Only explicitly active Packs contribute namespaced `//` identities. The machine-readable contracts are the [`Pack manifest v1`](https://github.com/EvanL1/AetherEdge/blob/main/contracts/pack/pack-manifest.v1.schema.json) and [`Pack asset index v1`](https://github.com/EvanL1/AetherEdge/blob/main/contracts/pack/pack-asset-index.v1.schema.json) schemas. Licensed under either MIT or Apache-2.0, at your option. --- # aether-ports Small, object-safe capability interfaces for Aether edge extensions. The crate separates authoritative live reads, acquisition-owned writes, device command dispatch, audit, history, mirroring, durable outbox, uplink publishing, I/O channel commissioning, and request-driven data processing. `ChannelMutator` keeps durable desired configuration authoritative and reports the rebuildable runtime projection, resulting revision, and reconciliation state without choosing a wire encoding. `HistoryQuery` and `CovariateSource` accept bounded logical windows and return source provenance; `DataProcessor` receives a complete `DataProcessingRequest` and has no callback into Aether data sources. It deliberately does not expose a generic database, cache, model, or script-runner API. Hosts choose concrete adapters at the composition boundary. `CloudLinkSpool` is intentionally separate from `DurableOutbox`: it owns stream epoch/position, stable batch identity/digest, replay and explicit loss evidence, and removes records only after a validated cloud application receipt. `CloudLinkTransport` carries bounded logical routes without exposing MQTT topic strings to core callers. There is no CloudLink command or arbitrary-RPC route. `HistoryQuery` bounds event time but does not implicitly promise bitemporal or source-epoch history; an implementation must declare stronger point-in-time semantics explicitly. Likewise, artifact chronology is not a history-port responsibility. Errors carry recovery semantics so callers can distinguish unavailable, transient, rejected, invalid-data, and permanent failures. ```bash cargo test -p aether-ports ``` Licensed under either MIT or Apache-2.0, at your option. --- # aether-edge-sdk Versioned beta facade for embedding the Aether AI-native IoT edge kernel. The API is release-gated for packaging and SemVer compatibility, but the first independent registry release has not yet been completed. Until then, consume it from a pinned repository revision rather than assuming crates.io availability. The Rust library target is imported as `aether_sdk`. `AetherBuilder` has no concrete infrastructure defaults. A host explicitly provides authoritative live state, a device-command dispatcher, and the mandatory audit sink. This keeps Redis, PostgreSQL, SQLx, web frameworks, and protocol drivers out of the SDK's default dependency graph. The `aether_sdk::pack` facade exposes the versioned, fail-closed domain-pack manifest loader. Loading a pack validates compatibility and confined asset directories; it never installs or commissions the pack. The optional `local-runtime` feature exposes zero-external-service adapters under `aether_sdk::local`. Downstream applications depend only on this facade; the workspace's domain, port, application, and adapter crates are source modules and do not define independent registry products. ```toml [dependencies] aether-sdk = { package = "aether-edge-sdk", git = "https://github.com/EvanL1/AetherEdge.git", tag = "v0.5.0", features = ["local-runtime"] } ``` For a runnable zero-external-service composition, see the repository's [`examples/minimal-gateway`](https://github.com/EvanL1/AetherEdge/tree/main/examples/minimal-gateway). ```bash cargo test -p aether-edge-sdk cargo test -p aether-edge-sdk --features local-runtime cargo run -p aether-example-minimal-gateway ``` Licensed under either MIT or Apache-2.0, at your option. --- # aether-testkit Reusable conformance checks and deterministic test doubles for Aether extension authors. The suites verify live-state round trips and ordered batch reads, FIFO and acknowledgement behavior for durable outboxes, bounded/provenance-preserving `HistoryQuery` projections, and exact descriptor/request/result correlation for request-driven `DataProcessor` implementations. Processor conformance also checks finite ordered forecast output and requires `unavailable` responses to contain no derived output. `MemoryCloudLinkTransport::pair` is the transport-neutral fake binding for session/replay tests. It emits transport-published evidence for durable sends but never invents a cloud application ACK, so tests must create the receipt explicitly. `ScriptedDataProcessor` is a queue-driven `DataProcessor` test double. It reports a configurable health result, consumes queued `ProcessingResult` or `PortError` values in FIFO order, and retains the complete `DataProcessingRequest` values it received. Application, adapter, and example tests can therefore prove both the exact frame sent to a processor and the result/error path without a model runtime or network service. Extension tests call the conformance helpers against concrete adapters so capability semantics remain consistent across local and external boundaries. ```bash cargo test -p aether-testkit ``` Licensed under either MIT or Apache-2.0, at your option. --- # aether-cloudlink-mqtt Broker-neutral MQTT v3.1.1/QoS 1 binding for the experimental CloudLink edge foundation. It validates a user-selected endpoint, TLS/authentication settings, topic prefix and gateway namespace; publishes with `retain = false`; subscribes only to the same gateway's session/ACK/replay topics; correlates QoS 1 PUBACK; and reconnects independently of local edge behavior. PUBACK is transport evidence only. The dedicated CloudLink spool is removed only by a validated application durable ACK. Default tests need no broker. See `docs/reference/cloudlink-mqtt-v1.md` for the opt-in shared-broker harness and environment variables. --- # Aether HTTP Data Processor Optional, bounded HTTP implementation of the `DataProcessor` port. It sends a complete request frame to `/v1/process`; it exposes no callback into Aether, SHM, history, or configuration. Plain HTTP is accepted only for a local processor on localhost or a loopback address. Remote routes require HTTPS. Request and response sizes and both connection and request timeouts are mandatory configuration. ## Composition ```rust use std::time::Duration; use aether_http_data_processor::{ BearerSecret, HttpDataProcessor, HttpDataProcessorConfig, }; use aether_ports::{DataBoundary, DataProcessorDescriptor}; # fn build( # descriptor: DataProcessorDescriptor, # deployment_token: String, # ) -> Result { let config = HttpDataProcessorConfig::new( "https://processor.example.net", descriptor, Duration::from_secs(2), Duration::from_secs(10), 4 * 1024 * 1024, )? .with_bearer_secret(BearerSecret::new(deployment_token)?); let processor = HttpDataProcessor::new(config)?; # assert_eq!(processor.descriptor().data_boundary(), DataBoundary::Remote); # Ok(processor) # } ``` Only the composition root selects the origin and optional secret. The adapter derives the fixed versioned routes: - `POST /v1/process` - `GET /v1/health` The request and successful processing response use `application/vnd.aether.data-processing+json;version=1`. Health accepts a small JSON response containing `status`, `processor`, `version`, and `contract`, and verifies those identity fields against the configured descriptor. ## Hard boundaries - Request JSON is encoded by `aether-data-processing` and checked against the descriptor's `max_frame_samples` and `max_request_bytes` before any network call. - Responses are bounded while streaming. `Content-Length` is an early guard, not the authority for the limit. - Redirects and ambient proxy discovery are disabled. - Remote routes require HTTPS. Local HTTP requires an explicit `Local` descriptor and a loopback or `localhost` origin. - URL credentials, query strings, fragments, non-origin paths, zero limits, and zero timeouts fail configuration with `Permanent`. - Bearer tokens have no environment-loading API, are marked sensitive in HTTP headers, and are redacted from all adapter `Debug` output. - Remote response bodies, URLs, and transport internals are never copied into port errors. Every 4xx or 5xx response must use the same versioned media type and the closed `aether.data-processing.error.v1` envelope. The body is size-bounded before decoding. Unknown fields, explicit nulls, malformed JSON, a mismatched HTTP status/category, an invalid or mismatched request ID, and inconsistent retry metadata fail closed as `InvalidData`. Validated failures map to stable `PortErrorKind` values: deadline errors to `Timeout`, a validated HTTP conflict response to `Conflict`, retryable capacity/unavailability to `Unavailable`, invalid frames to `InvalidData`, request/resource rejection to `Rejected`, and non-retryable authorization, lookup, internal, or unavailable failures to `Permanent`. Only the validated stable code, category, retryability, retry delay, and request ID enter the port diagnostic; the processor's free-form message and details are never copied. A response with `status: unavailable` is still a valid `ProcessingResult`, not a transport error. The `Conflict` mapping does not create request-replay semantics. The public `data_processing.process` operation is non-idempotent and has no built-in de-duplication or request-ID reuse guarantee. ## Verification ```bash cargo test -p aether-http-data-processor cargo clippy -p aether-http-data-processor --all-targets -- -D warnings cargo fmt --package aether-http-data-processor -- --check ``` --- # Aether HTTP History Query This optional extension maps semantic Data Processing history features to a loopback-only batch endpoint containing an already materialized cadence grid. It only accepts the task policy `aggregation=last` and `duplicate_policy=reject`; it does not aggregate raw observations. Mappings are scoped by exact task and binding identity. One physical upstream series may serve multiple tasks, while each `(task, binding, feature)` route is unique. The default `aether-history` collection stream is raw and therefore is not a valid source for a resampled forecast task through this adapter. The embedded SQLite adapter performs that production aggregation. This HTTP adapter is for deployments whose upstream service has already materialized the exact grid and can uphold the same contract. The wire response is event-time aligned; it carries no ingestion cut or source/configuration epoch. The adapter therefore does not claim point-in-time backtest semantics unless the upstream independently freezes and attests those cuts outside the v1 contract. The adapter rejects redirects, credentials, query strings, fragments, non-loopback cleartext endpoints, oversized responses, incomplete mappings, duplicate backend points, and timestamps outside the requested half-open window. Calendar features are generated deterministically from the requested grid; all other features require an explicit commissioned mapping. --- # aether-postgres-history Optional PostgreSQL implementation of Aether's append-only history capability. The extension owns its SQL schema and parameterized writes. It depends only on the public domain and port crates and is not part of the default SDK or edge composition. Live state remains authoritative SHM; PostgreSQL stores history only when the host explicitly selects this adapter. ```bash cargo test -p aether-postgres-history ``` Licensed under either MIT or Apache-2.0, at your option. --- # aether-redis-bridge Optional, non-authoritative Redis mirror for Aether point state. This extension implements the `StateMirror` capability. It is not enabled by `aether-sdk`, is not part of the default edge runtime, and must never be used as the control loop or live-state source of truth. Deploy it only when an external integration explicitly needs a Redis-shaped projection. ```bash cargo test -p aether-redis-bridge ``` Licensed under either MIT or Apache-2.0, at your option. --- # aether-shm-bridge Read-only capability bridge from Aether's authoritative shared-memory data plane to the public `LiveState` port. The bridge validates the logical channel manifest, supports per-consumer PointWatch bitmaps and UDS hints, reports channel health, and reconnects after writer restart or atomic SHM file replacement. It does not depend on Redis or PostgreSQL and never grants a consumer acquisition-writer authority. ```bash cargo test -p aether-shm-bridge ``` Licensed under either MIT or Apache-2.0, at your option. --- # Aether SQLite History Query Production `HistoryQuery` adapter for the embedded SQLite file owned by `aether-history`. It reads the existing table: ```text history(time_ms, series_key, point_id, value) ``` The adapter never initializes or migrates that schema. Construction is lazy, so a temporarily absent history database does not block the composition root. Each query opens or reuses SQLite in read-only mode plus `query_only=ON`; a missing or inaccessible file returns typed `Unavailable` and a later query can recover after the file appears. All SQL uses bound parameters. Read-only SQLite flags are not a filesystem security boundary. A production host must expose the historian database family (database plus WAL/SHM files) to the API reader through a dedicated read-only directory mount or an equivalent independently permissioned account/ACL. The current base Compose mounts all of `/app/data` read-write into `aether-api`, so it does not yet meet this defense-in-depth requirement and the production route remains blocked until a deployment override separates historian read access from the API's own writable configuration/audit database. Each commissioned route fixes the exact task identity, binding identity, feature definition (including unit), task cadence, core `HistoryAggregation`, and physical series. There is no implicit aggregation default: the route and `HistoryWindow` must declare the same policy. Duplicate handling is likewise task-owned: `Latest` keeps the greatest SQLite `rowid` at a timestamp before aggregation, while `Reject` fails with typed invalid data. A logical `(task, binding, feature)` route is unique. The same physical `(series_key, point_id)` may be reused by different tasks so each task can apply its own commissioned cadence and aggregation policy. For a logical interval-end label `t`, raw numeric observations in `(t - cadence, t]` are reduced with the commissioned `Mean`, `Last`, `Sum`, `Min`, or `Max` policy and stamped at `t`. The EMS load route explicitly uses `Mean`. The output is always the complete requested grid; a bucket without numeric observations is represented by `FeatureValue::missing()` and `SampleQuality::Missing`. Null rows do not participate in aggregation or advance provenance. A stored feature's watermark is the maximum timestamp of a numeric raw observation that actually participated in aggregation—not the bucket label. Reads never advance beyond `HistoryWindow::cutoff()`. Every stored-feature query requests `max_raw_samples_per_feature + 1` rows. If the extra row exists, the operation fails closed instead of silently truncating raw input. Calendar features, including UTC quarter-hour-of-day, are generated deterministically from the same interval-end grid. All feature reads in one logical query share one read transaction and therefore one SQLite snapshot. That snapshot is read-consistent at invocation time; it is not a historical point-in-time snapshot for `as_of`. The current table has neither an `ingested_at`/system-time column nor a source or configuration epoch. A row backfilled after an old `as_of` can therefore appear in a later replay, and a device remapped behind the same `(series_key, point_id)` can concatenate old and new physical sources. Task and binding revisions fail closed against the current route, but cannot filter epochs that were never stored with the rows. Use a frozen database/export captured at the evaluation cut for an offline golden or backtest. A rigorous point-in-time adapter needs bitemporal ingestion metadata plus a stored source/binding epoch and queries both cuts. The adapter path is also deployment configuration, not proof of the active historian writer. `history_config.storage_*` records saved intent, while `PUT /hisApi/storage` does not reconnect the running history backend. Disable Data Processing across a storage change, reconnect or restart `aether-history`, verify the active SQLite backend and a commissioned sentinel series, then restart `aether-api` with the same path. The current authority check cannot by itself distinguish saved intent from an unreconnected writer. ```rust,no_run use aether_domain::{ BindingIdentity, FeatureDefinition, FeatureRole, HistoryAggregation, HistoryDuplicatePolicy, TaskIdentity, }; use aether_sqlite_history_query::{ SqliteHistoryFeatureRoute, SqliteHistoryQuery, SqliteHistoryQueryConfig, }; # async fn example() -> Result<(), Box> { let task = TaskIdentity::new("energy.site-load-forecast", 1)?; let binding = BindingIdentity::new("site-a", 1)?; let load = FeatureDefinition::numeric("load", FeatureRole::History, "kW")?; let route = SqliteHistoryFeatureRoute::stored( task, binding, load, 900_000, HistoryAggregation::Mean, HistoryDuplicatePolicy::Latest, "inst:1:M", "101", "site.load.active_power", )?; let config = SqliteHistoryQueryConfig::new( "/var/lib/aether/aether-history.db", vec![route], 100_000, )?; let history = SqliteHistoryQuery::open(config).await?; # let _ = history; # Ok(()) # } ``` Run its real-SQLite contract tests with: ```bash cargo test -p aether-sqlite-history-query ``` --- # aether-store-local Local adapters for a gateway that must run without external services. | Adapter | Persistence | Intended use | |---|---|---| | `MemoryLiveState` | process-local | SDK embedding, tests, small compositions | | `MemoryHistorySink` | process-local | tests and host-managed persistence | | `MemoryHistoryQuery` | process-local | bounded logical history fixtures for Data Processing | | `MemoryCovariateSource` | process-local | known-future covariate fixtures for Data Processing | | `SnapshotCovariateSource` | atomically replaceable JSON | production known-future covariates without an external service | | `MemoryAuditSink` | process-local | tests and host-managed persistence | | `SqliteAuditSink` (`sqlite-audit`) | embedded SQLite | mandatory command audit without an external service | | `MemoryOutbox` | process-local | conformance tests and ephemeral workloads | | `FileOutbox` | crash-recoverable file | production offline store-and-forward | | `MemoryCloudLinkSpool` | process-local | deterministic application-ACK/replay conformance | | `FileCloudLinkSpool` | crash-recoverable file | experimental CloudLink positions, replay, and loss evidence | `MemoryHistoryQuery` and `MemoryCovariateSource` are keyed by the complete versioned `BindingIdentity`. They project only requested logical features in request order, apply half-open time windows and hard sample limits, and retain one exact provenance entry per returned feature. Unknown bindings are permanent commissioning errors; an empty selected window remains an availability outcome. These read adapters are deliberately separate from `MemoryHistorySink`. Querying or replacing a deterministic fixture does not mutate the append-only history sink and never changes SHM live-state authority. ## SnapshotCovariateSource `SnapshotCovariateSource` is the production zero-service adapter for forecast covariates such as weather predictions. Construction retains only the path and hard limits, so a missing optional snapshot does not prevent the host from starting. Every forecast resolution reads the currently published file on a blocking worker, applies the byte bound before parsing, and fully validates it. ```rust use aether_store_local::{SnapshotCovariateLimits, SnapshotCovariateSource}; # fn example() -> Result<(), Box> { let limits = SnapshotCovariateLimits::new( 4 * 1024 * 1024, // file bytes 256, // bindings 32, // runs per binding 64, // features per run and response 4_096, // samples per run and response )?; let source = SnapshotCovariateSource::open("./data/covariates.json", limits)?; # let _ = source; # Ok(()) # } ``` The JSON shape is strict: unknown fields and unknown enum values are rejected. Timestamps are UTC Unix milliseconds. A run has one issue time, one source watermark, one exact valid-time grid, and a redaction-safe logical source reference for every nondeterministic feature. ```json { "schema": "aether.covariate-snapshot.v1", "bindings": [ { "id": "example-site", "revision": 1, "runs": [ { "issued_at_ms": 1783741200000, "watermark_ms": 1783741800000, "valid_times_ms": [1783743300000, 1783744200000], "features": [ { "name": "temp_avg", "value_type": "number", "unit": "Cel", "source_ref": "weather.nwp.air_temperature", "values": [32.1, 32.0], "quality": ["good", "good"] } ] } ] } ] } ``` `value_type` is `number`, `string`, or `boolean`; non-numeric features omit `unit`. Values may be `null` only when the matching quality is `missing`. Quality is `good`, `uncertain`, `substituted`, or `missing`. For a requested `as_of`, the adapter selects the newest run whose `issued_at_ms <= as_of`. It never silently falls back to an older run when the newest eligible run has the wrong grid, type, or unit. Its selected watermark must also be at or before `as_of`. The requested half-open window and sample count define an exact regular grid; missing, extra, or off-grid valid times are an `InvalidData` outcome rather than a truncated response. For a v1 interval-end forecast with cadence `c`, that future grid begins at `as_of+c`. The current energy load/PV tasks require `issued_at` for every non-calendar future covariate. `quarter_hour` is reserved and must not be stored in the file. When requested as a numeric future covariate with unit `1`, it is generated deterministically from UTC valid time, with `calendar.utc.quarter_hour` provenance and the request `as_of` as its watermark. Publish updates by writing and syncing a sibling temporary file, then renaming it over the configured path on the same filesystem. The next resolution sees the new run set. A missing file returns `Unavailable`; an invalid update returns `InvalidData` (or `Rejected` for a hard bound) and never reuses a stale in-memory snapshot. Avoid in-place writes, which can expose a partial file to a concurrent reader. ## FileOutbox ```rust use std::sync::Arc; use aether_ports::{DurableOutbox, OutboxMessage}; use aether_domain::TimestampMs; use aether_store_local::FileOutbox; # async fn example() -> Result<(), Box> { let outbox: Arc = Arc::new(FileOutbox::open("./data/uplink.outbox", 10_000)?); outbox .enqueue(OutboxMessage::new( "telemetry/site-a", br#"{"temperature": 21.5}"#.to_vec(), TimestampMs::new(1_700_000_000_000), )) .await?; # Ok(()) # } ``` Each successful mutation has been synchronized to the journal. Recovery replays complete checksum-valid records and treats an incomplete or checksum-invalid final record as a crash-torn tail. Corruption before a later committed record fails closed instead of discarding the later data. The journal permits one process writer, is bounded by entry count, and can be reclaimed with `FileOutbox::compact()`. Long-running hosts should invoke compaction periodically; the compatibility `uplink` does so at startup and hourly. Capacity bounds live entries, while compaction bounds obsolete acknowledged records in the journal. Disk durability does not define network delivery. The selected `UplinkPublisher` decides when an entry may be acknowledged. ## CloudLink spools `MemoryCloudLinkSpool` and `FileCloudLinkSpool` implement the dedicated `CloudLinkSpool` port. They preserve stream epoch, monotonic position, stable batch identity/digest, offer/PUBACK state, last durable application ACK, and capacity-overflow data-loss evidence. A transport publish never removes a record. Stale-session, wrong-stream, wrong-batch, and wrong-digest ACKs fail closed; an exact duplicate ACK is idempotent. The file adapter owns an exclusive process lock and synchronizes every state transition in an incremental journal. Recovery truncates only an incomplete tail; a checksum or semantic failure is corruption and fails closed even in the last complete record. `FileCloudLinkSpool::compact()` atomically rewrites cursor metadata plus live records, and the adapter compacts before accepting more work after 256 mutations. Its file format is independent of legacy `FileOutbox` and cannot be opened through the generic outbox port. --- # Connect AI Assistants The `aether` CLI doubles as an MCP (Model Context Protocol) server: `aether mcp` runs over stdio and exposes the system's capabilities as tools, so Claude — or any MCP client — can inspect channels, query history, read alarms, and (when explicitly allowed) operate the system. This page covers client setup, pointing the server at a remote installation, and the read-only/write access model. ## What you get The production MCP catalog has 45 tools in two tiers: - **23 read-only tools**, always registered — listing and inspecting channels and their point mappings (`channels_list`, `channels_status`, `channels_points`), alarms and alarm rules (`alarms_list`, `alarms_stats`), control rules (`rules_list`, `rules_get`), routing, historical data (`history_query`, `history_latest`), product models and device instances (`models_products`, `models_instances`), channel templates, and cloud-link status (`net_mqtt_status`, `net_cert_info`). - **22 governed write tools**, registered only when the server is started with `--allow-write`: `channels_create`, `channels_update`, `channels_delete`, `channels_enable`, `channels_disable`, and `channels_reconcile`; `models_instances_action`; `rules_execute`; `rules_create`, `rules_update`, `rules_delete`, `rules_enable`, and `rules_disable`; `alarms_rule_create`, `alarms_rule_update`, `alarms_rule_delete`, `alarms_rule_enable`, and `alarms_rule_disable`; `alarms_resolve`; and `routing_action_upsert`, `routing_action_delete`, and `routing_action_set_enabled`. They map respectively to the governed `io.channel.manage`, `io.channel.reconcile`, `device.write_point`, `automation.rule.execute`, `automation.rule.manage`, `alarm.rule.manage`, `alarm.alert.resolve`, and `automation.routing.manage` application capabilities. Every write requires a signed identity, `confirmed: true`, application authorization, and mandatory audit. See [Read-only vs write access](#read-only-vs-write-access) below. Each tool wraps one CLI client call against the same service HTTP APIs the `aether` command line uses. Results come back as structured content; a failed or unreachable service comes back as readable error text rather than an opaque protocol error. The server also serves the documentation you are reading now as MCP [resources](#resources), so an assistant can learn the domain — what a PCS is, which writes reach real hardware — without leaving the session. One flag note: the CLI's global `--json` flag is ignored for `mcp` (the server always speaks MCP's own JSON-RPC protocol) and prints a warning if passed. ## Claude Desktop Add to `claude_desktop_config.json` (the `aether` binary must be on `PATH`, or use an absolute path): ```json { "mcpServers": { "aether": { "command": "aether", "args": ["mcp"] } } } ``` ## Claude Code ```bash claude mcp add aether -- aether mcp ``` For a session that needs write access (see the access model below): ```bash claude mcp add aether -- aether mcp --allow-write ``` ## Pointing at a remote system The MCP server does not have to run on the edge device. Every tool talks to the Aether service APIs, so `aether mcp` on a laptop can inspect a remote installation. Two mechanisms are resolved at server startup: - **`--host `** rewrites the host for all five service URLs while keeping plaintext HTTP and the default ports. This is a quick path for read-only tools when all services run on one trusted network host: ```bash aether mcp --host 192.168.1.50 ``` - **Five environment variables** set each service URL independently, useful when schemes, ports, or hosts differ per service: | Environment variable | Service | Tools served | Default | |----------------------|---------|--------------|---------| | `AETHER_IO_URL` | io | channels, points, templates | `http://localhost:6001` | | `AETHER_AUTOMATION_URL` | automation | rules, routing, models/instances | `http://localhost:6002` | | `AETHER_ALARM_URL` | alarm | alarms | `http://localhost:6007` | | `AETHER_UPLINK_URL` | uplink | MQTT, certificates | `http://localhost:6006` | | `AETHER_HISTORY_URL` | history | history | `http://localhost:6004` | Precedence: `--host` wins — when it is passed, the environment variables are not consulted. When neither is set, everything defaults to `localhost`. Protected writes carrying `AETHER_ACCESS_TOKEN` are allowed over loopback HTTP for on-device operation. For any remote write-enabled MCP server, omit `--host` and point the service variables at certificate-validated HTTPS ingresses. The transport guard rejects non-loopback plaintext HTTP before the Bearer token is selected for the request or attached. In the Claude Desktop config, a remote write-enabled server can use the `env` block like this (replace the example hostnames with your ingress endpoints): ```json { "mcpServers": { "aether-site-a": { "command": "aether", "args": ["mcp", "--allow-write"], "env": { "AETHER_IO_URL": "https://io.edge.example.test", "AETHER_AUTOMATION_URL": "https://automation.edge.example.test", "AETHER_ALARM_URL": "https://alarm.edge.example.test", "AETHER_UPLINK_URL": "https://uplink.edge.example.test", "AETHER_HISTORY_URL": "https://history.edge.example.test", "AETHER_ACCESS_TOKEN": "" } } } } ``` ## Read-only vs write access By default, `aether mcp` is read-only. This is not an advisory annotation: without `--allow-write`, the 22 write tools are never registered and do not appear in the `tools/list` response at all. A client cannot call — or even see — what is not registered, so the guarantee holds regardless of how the client is configured or how the model behaves. Starting the server with `--allow-write` is a deliberate act, but the flag is only a registration gate. It is not confirmation for any command. The MCP caller must still pass `confirmed: true` on every invocation, and the application rejects unauthorized or unauditable requests before dispatch. The MCP bridge reads `AETHER_ACCESS_TOKEN`, sends it to the service as an `Authorization: Bearer` credential, and generates an `X-Request-ID` for each governed request. It refuses to attach that credential to non-loopback plaintext HTTP; remote writes require a certificate-validated HTTPS ingress. Preserve the returned `request_id` and any `command_id`: timeouts and incomplete audit or publication responses are not safe automatic retry signals. A successful device-command response means the local command plane accepted the command; it does not prove that the physical device executed it. A routing response means the physical target was persisted and published; it does not execute a device command. **Before enabling writes, read [Safe Operations for Applications and Agents](/en/guides/safe-operations).** If a command response reports `audit.status="incomplete"`, the command was already accepted: retain its `request_id`/`command_id` and do not retry it. Channel mutation success can also report a degraded runtime projection. Retain its `request_id` and `resulting_revision`, inspect `reconciliation_required`, and do not automatically retry the non-idempotent commissioning command. Channel simulation/point-batch and uplink configuration/certificate operations remain outside MCP. Channel CRUD/lifecycle, rule CRUD/lifecycle, alarm-rule CRUD/lifecycle, and alert resolution are present only because their schemas and application capabilities are explicitly mapped in the 22-tool write allowlist. No existing wrapper is promoted to an AI tool merely because `--allow-write` is present. The one-line rule: **give an assistant write access for a task, not as a default.** Register the write-enabled server for the session that needs it, and drop back to read-only afterward. ## Resources Beyond tools, the server serves a curated subset of this documentation as read-only MCP resources in both modes. Kernel pages are embedded; Pack knowledge appears only when that validated Pack is active in `global.yaml`. Clients that support MCP resources can pull context directly instead of relying on the model's prior knowledge: - `aether://packs/energy/knowledge/ess-primer` — energy-storage concepts when the Energy Pack is active - `aether://packs/energy/knowledge/safe-operations` — the Energy Pack safety contract - `aether://docs/concepts/architecture` — the seven services and how they talk - `aether://docs/concepts/data-model` — instances, channels, points - `aether://docs/reference/mcp-tools` — the full tool reference ## Related pages - [Safe Operations for Applications and Agents](/en/guides/safe-operations) — read this before `--allow-write` - [System Architecture](/en/concepts/architecture) — the services behind the tools - [MCP Tools Reference](/en/reference/mcp-tools) — every tool with its parameters - [Getting Started](/en/guides/getting-started) — build, initialize, and start the stack the tools talk to --- # Build Applications with AI AetherEdge is headless by design. It provides a deterministic edge runtime and the machine-readable contracts an AI agent needs to build a site-specific application. A generated Web UI, mobile app, CLI, or backend is a replaceable client: it is never part of the live-state, desired-state, or safety authority. This guide defines the development method shared by reference applications and downstream products. ## Optional Agent Skill Optionally add the repository's `aether-iot` Skill to a compatible coding assistant. Bun is used only to run the third-party `skills` CLI; this command does not install the AetherEdge runtime or Rust SDK: ```bash bunx skills add EvanL1/AetherEdge --skill aether-iot ``` The Skill is deliberately small. It teaches the workflow and routes the agent to the current online documentation instead of embedding a stale copy. The documentation service publishes: - `/llms.txt` — the curated document index; - `/llms-full.txt` — the complete public corpus; - a Markdown twin for every page by appending `.md` or sending `Accept: text/markdown`. Connect `aether mcp` when the agent also needs live runtime information. The default MCP surface is read-only; do not enable writes merely to generate an application. ## Use the contract stack An application should discover AetherEdge in this order: | Source | What it establishes | |---|---| | Runtime manifest | Exact Kernel version, target, features, protocols, and capabilities | | Active Pack | Domain vocabulary, supported assets, and compatibility requirements | | OpenAPI | Exact HTTP paths, schemas, status codes, authentication, and command policy | | MCP | Live tools and structured runtime results available to an AI client | | Online Markdown | Architecture, workflows, safety guidance, and domain explanation | Do not substitute a README example or model memory for a running release's OpenAPI document. Do not assume that an installed Pack, optional adapter, or write capability exists until its versioned contract says so. ## Start from a read-only user story Write the first prompt as a bounded outcome, for example: ```text Build a read-only operations page for this AetherEdge site. Show service health, the current topology revision, point values with quality and freshness, active alarms, and the last hour of available history. Do not add device controls. ``` Before generating code, the agent should produce a short capability inventory: 1. runtime and Pack version; 2. queries and subscriptions required by the page; 3. public capabilities that satisfy them; 4. capabilities that are missing or still internal; 5. the exact OpenAPI documents used for client generation. A missing public query is an application-boundary gap. It is not permission to expose an internal service port, scrape SQLite, attach to SHM, or invent an endpoint. ## Keep one remote boundary Only `aether-api` is intended for remote application traffic. It publishes fixed authenticated namespaces under `/api/v1/io`, `/api/v1/automation`, `/api/v1/history`, `/api/v1/uplink`, and `/api/v1/alarm`; their owning process APIs remain on loopback. A generated remote client must therefore: - call those gateway-prefixed capabilities only through authenticated `aether-api` on port 6005; - use the running gateway's OpenAPI contract to generate types and requests; - use its authenticated WebSocket contract for live updates when available; - report an unavailable use case instead of proxying an internal port; - keep credentials out of source control, URLs, logs, and browser persistence not designed for secrets. Local development tools may inspect loopback OpenAPI documents on the edge host, but this does not turn those ports into supported remote interfaces. ## Preserve IoT state semantics A useful IoT screen needs more than a numeric value. Preserve and display contract fields for: - point identity and engineering unit; - value plus health or quality; - source timestamp and observed freshness; - physical topology epoch; - desired configuration revision; - active, degraded, or reconciliation-pending runtime state. Never silently coerce missing, stale, unhealthy, or generation-mismatched data to zero. Never merge desired state and active runtime state into one optimistic status. When a topology change is in progress, keep the last coherent generation visible or show that a coherent view is unavailable. Presentation names, groups, units, enums, and recommended visualizations belong to the active Domain Pack or the downstream application. A generic client must not infer energy semantics from the AetherEdge kernel. ## Add commands as a separate phase Introduce a command only after the read-only workflow is complete. For every command, render and enforce the server-declared contract: - risk and required permission; - explicit confirmation policy; - idempotency and expected-revision behavior; - request correlation and audit result; - accepted, degraded, and uncertain outcomes. Keep the returned `request_id`, `command_id`, and resulting revision. Do not automatically retry a non-idempotent command after a timeout, incomplete audit, accepted-but-degraded response, or unknown physical outcome. Device-command acceptance proves that the local command plane accepted the request; feedback telemetry is required to show physical execution. The backend enforces these rules. A confirmation dialog is useful presentation, but it is never the security boundary. ## Verify the generated application Every maintained reference or downstream application should pass: 1. type checking, linting, unit tests, and a production build; 2. contract fixtures for healthy, stale, unauthorized, degraded, and uncertain outcomes; 3. a check that no request targets direct process ports, SHM, or SQLite; 4. a read-only default in which control components and write credentials are absent; 5. an integration check against a safe-empty or simulated AetherEdge composition. Reference applications demonstrate the contract and can be replaced. They do not define API behavior, configuration authority, or domain truth. ## Related pages - [Agent Quickstart](https://docs.aetheriot.workers.dev/agent-quickstart/) — install and connect from zero - [Connect AI Assistants](/en/guides/ai-assistants) — MCP setup and write gating - [HTTP API](/en/reference/http-api) — public exposure and operation contracts - [System Architecture](/en/concepts/architecture) — service and data boundaries - [Getting Started](/en/guides/getting-started) — safe-empty runtime setup --- # Connect Devices A device attaches to Aether as a **channel** in the communication service (io, port 6001). A channel is one device connection: a protocol, the transport parameters that protocol needs, and a point table describing what the device exposes. Channel points then map to device **instances** — the logical thing-model that rules and dashboards work against (see [Data Model](/en/concepts/data-model)). ## Channels Channels are authored in `config/io/io.yaml` and loaded into SQLite by `aether sync`; services never read the YAML directly. A trimmed example from the shipped template (`config.template/io/io.yaml`), showing one TCP and one serial connection: ```yaml channels: - id: 1 name: "PCS#1" protocol: "modbus_tcp" enabled: true parameters: host: "192.168.1.10" port: 502 connect_timeout_ms: 3000 read_timeout_ms: 3000 - id: 3 name: "GENSET#1" protocol: "modbus_rtu" enabled: true parameters: device: "/dev/ttyS4" baud_rate: 9600 data_bits: 8 stop_bits: 1 parity: "N" ``` The `parameters` block is protocol-specific: Modbus TCP wants a host and port, Modbus RTU wants a serial device and line settings, MQTT wants a broker URL and subscription topics, and so on. Protocol names are normalized before matching (`normalize_protocol_name` in `services/io/src/utils.rs`), so `modbus-tcp`, `ModbusTCP`, and `modbus_tcp` all resolve to the same protocol. Channels can also be created at runtime without touching YAML: ```bash aether channels create --name "PCS#2" --protocol modbus_tcp \ --params '{"host": "192.168.1.11", "port": 502}' ``` which calls `POST /api/channels` on io. `aether channels list`, `update`, `delete`, `enable`, and `disable` cover the rest of the lifecycle. Each channel carries a point table split by the four point types — telemetry (T, analog measurement), signal (S, digital status), control (C, digital command), and adjustment (A, analog setpoint). Points are managed with `aether channels points list|add|update|delete` or authored as CSV tables next to the channel YAML and picked up by `aether sync`. ## Protocol availability io speaks 14 protocols, but most are behind compile-time Cargo features (`services/io/Cargo.toml`), so a given binary usually contains only a subset. The default feature set compiles Modbus, GPIO, Aether-485, IEC 61850, and CAN. | Protocol | Compiled by default | Platform notes | |----------|--------------------:|----------------| | Modbus TCP/RTU (`modbus`) | yes | | | IEC 60870-5-104 (`iec104`) | no | | | IEC 61850 MMS (`iec61850`) | yes | | | OPC UA (`opcua`) | no | Optional feature; currently restricted to anonymous `SecurityPolicy::None` sessions. | | MQTT (`mqtt`) | no | event-driven JSON payloads; enabling pulls in `json-mapping` | | HTTP (`http`) | no | polling and webhook modes; enabling pulls in `json-mapping` | | DL/T 645-2007 (`dl645`) | no | smart meters over serial or TCP | | CAN (`can`) / J1939 (`j1939`) | CAN yes, J1939 no | Linux only; `j1939` implies `can` | | GPIO (`gpio`) | yes | Linux only | | BLE GATT (`ble`) | no | | | Zigbee (`zigbee`) | no | via TCP gateway | | Matter (`matter`) | no | | | Aether-485 (`aether_485`) | yes | private RS-485 protocol | | Virtual | always | no feature gate; exists for testing and simulation | Two protocols are additionally OS-gated in the channel factory (`services/io/src/protocols/gateway/factory.rs`): CAN and GPIO are compiled only on Linux, so they never exist in a macOS build regardless of features. Virtual is the one protocol with no gate at all — it is always available, and it is the right first target for trying out rules and mappings before real hardware is involved. The rule of thumb: **if a channel fails to create, check the feature gate first.** The factory's error is literal about it — `Unsupported protocol: {name}. Check if the required feature is enabled.` — and the cause is almost always a protocol that was not compiled in, not a configuration typo. ## Mapping points to instances Channel points are protocol-flavored (register 62001 on channel 2); rules and dashboards want model-flavored values (battery pack state of charge). The bridge is an instance plus routing: 1. **Define the instance.** An instance binds a device to a product template in `config/automation/instances.yaml`. The default distribution intentionally starts empty; optional examples live under `packs/energy/examples/config/automation/instances.yaml`: ```yaml instances: pcs_01: product_name: PCS name: "PCS #1" properties: rated_power: 500.0 rated_voltage: 380.0 ``` The product defines which measurement points and action points the instance has; the properties fill in the template's static values. 2. **Map channel points to instance points.** Routing wires a channel point to an instance point: telemetry and signal points feed instance measurement points (M, the `route:c2m` table), and instance action points (A) drive channel control and adjustment points (`route:m2c`). Entries can be created through the CLI: ```bash aether routing create 1 --point-type M --point-id 9 \ --channel-id 1 --four-remote T --channel-point-id 101 ``` which calls `POST /api/instances/{id}/routing` on automation, or in bulk with `aether routing batch`. 3. **Run `aether sync`** if the instance or routing was authored in YAML. Sync validates the configuration and writes it into SQLite, where the services load it; `--dry-run` validates without writing. 4. **Verify.** Two checks, one per side of the bridge: ```bash aether channels unmapped-points 1 # channel side aether routing list --channel 1 # instance side ``` The first (`GET /api/channels/{id}/unmapped-points` on io) lists points declared on the channel whose protocol mapping is still empty — points io cannot poll because they are not yet wired to a protocol address. The second shows every routing entry touching the channel, so a forgotten instance binding stands out as a missing row. ## Verifying a connection Check the channel status first: ```bash aether channels status 1 ``` This calls `GET /api/channels/{id}/status` and returns `connected`, `running`, `last_update`, and cumulative statistics (read/write counts, average response time). Note that `connected` checks both the transport state and data freshness: a channel that holds its TCP connection but has received no data for 90 seconds reports `false`. Then watch a live value. On the channel side, `GET /api/channels/{channel_id}/{T|S|C|A}/{point_id}` returns the current value with its timestamp and raw protocol value. For direct inspection, open the shared-memory REPL: ```bash aether shm ``` If the channel point updates but the instance point does not, the routing entry is missing or wrong. SHM is the authoritative live view, so no external database needs to be running for this check. What offline looks like: `aether channels status` reports `connected: false`, the channel-health SHM entry becomes offline, and point values stop updating — their timestamps go stale. A point that has *never* been acquired is a NaN sentinel in shared memory, not a zero; see [Data Model](/en/concepts/data-model) for why unavailability is a first-class value. For a whole-system pass — services up, SQLite readable, shared memory attached — run `aether doctor`. ## Related pages - [Data Model](/en/concepts/data-model) — products, instances, and the four point types - [System Architecture](/en/concepts/architecture) — where io and automation sit and how data flows between them - [Writing Rules](/en/guides/writing-rules) — putting mapped points to work in control logic --- # Connect Data Processors This page explains the implemented integration pattern for **Aether Data Processing**. The core types, application orchestration, v1 codec, local test adapters, bounded HTTP adapter, schemas, and energy-pack examples are present in this repository. The opt-in `aether-api` composition reads a strict runtime configuration; the complete synthetic template is [`packs/energy/data-processing/runtime.example.yaml`](https://github.com/EvanL1/AetherEdge/blob/main/packs/energy/data-processing/runtime.example.yaml). A data processor receives a complete, governed input frame and returns derived data. It does not reach back into Aether to discover its own inputs. This makes the same processor usable as an in-process adapter, a local sidecar, or an approved remote service without changing data ownership. ```text caller │ ▼ DataProcessingApplication ├─ task declaration from a domain pack ├─ HistoryQuery ├─ LiveState (read-only) ├─ CovariateSource └─ deterministic transforms │ ▼ ProcessingFrame │ request ▼ DataProcessor │ ▼ ProcessingResult (untrusted) │ validate and stamp ▼ DerivedData ``` The split is deliberate: - Aether owns point identity, source selection, time alignment, unit/sign contract validation, quality policy, authorization, and auditing. - A `DataProcessor` owns processor-specific feature ordering, scalers, tensor construction, model execution, and output post-processing. - A domain pack declares the semantics of a task. It does not select a concrete endpoint or contain credentials. - Derived data is not live device state. A processor cannot write SHM, history storage, or device commands. ## Declare a task in a domain pack Place task declarations with the industry knowledge that defines their input and output semantics. The repository convention is `packs//data-processing/tasks/*.yaml`; the energy pack ships complete, disabled-by-default load and PV examples there. A composition root or commissioning loader translates validated assets into enabled routes. The following load-forecast task is illustrative: ```yaml schema: aether.data-processing-task.v1 id: energy.site-load-forecast revision: 1 kind: forecast processor_contract: aether.data-processing.forecast.v1 target: name: load semantic_point: site.load.active_power unit: kW sign_convention: positive_consumption frame: cadence_seconds: 900 history_steps: 672 horizon_steps: 288 timezone: UTC live_tail: forbidden inputs: history: - name: load unit: kW source: kind: measurement instance_ref: site_load point_ref: active_power - name: temp_avg unit: Cel source: kind: covariate dataset_ref: weather.observed field: air_temperature - name: humidity unit: "%" source: kind: covariate dataset_ref: weather.observed field: relative_humidity - name: rain unit: mm source: kind: covariate dataset_ref: weather.observed field: precipitation - name: quarter_hour unit: "1" source: kind: calendar transform: quarter_hour_of_day_zero_based future_covariates: - name: temp_avg unit: Cel source: kind: covariate dataset_ref: weather.nwp field: air_temperature - name: humidity unit: "%" source: kind: covariate dataset_ref: weather.nwp field: relative_humidity - name: rain unit: mm source: kind: covariate dataset_ref: weather.nwp field: precipitation - name: quarter_hour unit: "1" source: kind: calendar transform: quarter_hour_of_day_zero_based alignment: aggregation: mean timestamp_semantics: interval_end duplicate_policy: latest feature_policies: - { name: load, aggregation: mean, duplicate_policy: latest } - { name: temp_avg, aggregation: mean, duplicate_policy: latest } - { name: humidity, aggregation: mean, duplicate_policy: latest } - { name: rain, aggregation: sum, duplicate_policy: latest } - { name: quarter_hour, aggregation: last, duplicate_policy: reject } missing_policy: reject quality: max_input_age_seconds: 900 max_gap_seconds: 1800 max_missing_ratio: 0.0 require_input_watermark: true require_covariate_issue_time_not_after_as_of: true output: unit: kW sign_convention: positive_consumption max_quantiles: 0 expires_after_seconds: 3600 ``` ### What belongs in the task The declaration should contain only portable domain semantics: - a stable task ID, revision, and typed task kind; - semantic instance and point references rather than channel IDs, SHM slots, SQL names, or vendor register addresses; - canonical units and sign conventions; - historical and known-future features; - cadence, window, alignment, and missing-data rules; - the expected derived-data shape and freshness limit; and - the processor contract the task requires. Site commissioning resolves `instance_ref` and `point_ref` to the actual instances and routes. Pack validation must fail closed if a required reference cannot be resolved or its physical unit, scale, offset, point kind, or target sign convention does not exactly match the commissioned task. The current v1 runtime validates those facts; it does not perform engineering-unit or sign conversion. The declaration must not contain: - a processor URL, process name, API token, or TLS key; - a concrete history database query; - a SHM path or layout assumption; - an ONNX/RKNN input node name; or - a device action to execute from the result. Those details belong to composition, an adapter, or a separate control use case. ## Implement a `DataProcessor` The implemented port is intentionally narrow. In Rust-like pseudocode: ```rust #[async_trait] pub trait DataProcessor: Send + Sync { fn descriptor(&self) -> &DataProcessorDescriptor; async fn health(&self) -> PortResult; async fn process( &self, request: DataProcessingRequest, ) -> PortResult; } ``` The request and result are typed contracts documented in [Data Processing Contracts](/en/reference/data-processing-contracts). A processor descriptor declares supported contract versions, task kinds, the local/remote data boundary, and finite frame/request limits. It must not expose a generic vendor command set or an unvalidated `run(json)` escape hatch. A processor is responsible for: - rejecting unsupported contract versions, task kinds, features, and shapes; - applying the feature order and normalization owned by the selected model; - executing its deterministic algorithm or model runtime; - returning the actual model version and artifact digest; - distinguishing a normal result, an explicit fallback, and no usable result; and - honoring deadlines and bounded resource limits. A processor must not: - query Aether SHM, SQLite, the history service, or a domain-pack directory; - accept only a `plant_id` and then discover the corresponding data itself; - infer units or power direction from a site name; - publish derived values as ordinary live measurements; - dispatch a device command; or - turn a processing failure into an apparently normal zero-valued result. For model-backed processing, keep model internals on the processor side. The processor may own feature order, scaler statistics, sequence length, model artifacts, ONNX/RKNN selection, and de-normalization. Aether sends named, unit-bearing observations instead of model tensors. ## Configure local and remote processors Only a composition root chooses a concrete processor. The strict runtime YAML contains the full task, binding, history route, covariate source, and processor descriptor. This abbreviated fragment shows only the processor portion; do not use it as a complete configuration. `HttpDataProcessorConfig` receives a validated endpoint and derives the fixed `/v1/process` and `/v1/health` routes: ```yaml processor: endpoint: http://127.0.0.1:8989/ id: load-forecasting-edge version: 0.1.0 contract: aether.data-processing.forecast.v1 requires_artifact: true boundary: local max_frame_cells: 5000 max_request_bytes: 4194304 connect_timeout_ms: 500 request_timeout_ms: 4500 max_response_bytes: 4194304 bearer_token_env: AETHER_LOAD_FORECASTING_BEARER_TOKEN ``` The generic domain/processor contract supports static features, and custom in-process compositions can bind them through `DataProcessingBinding`. The current `aether-api` runtime YAML loader has no static-value binding field, so a runtime-configured v1 route must not declare static features. Add loader support and tests before commissioning one; do not assume the wire schema alone makes it available. Local and remote adapters use the same request/result contract. A remote route adds an explicit data-egress boundary and must be preapproved by both task and route policy. Secrets stay in environment-owned secret injection. The current configuration has no custom CA-file setting; HTTPS uses the HTTP client's configured trust roots. A deployment needing private trust material must provide it through a supported transport composition rather than inventing a YAML key. The default Aether distribution must not require either processor. With no route configured, the task is unavailable and the rest of acquisition, history, alarms, rules, and device control continue independently. ## How Aether assembles a request `DataProcessingApplication` owns the following steps for each request, with task-specific transforms supplied by the commissioned assembly composition: 1. Authorize the advertised processing capability and load the task revision. 2. Resolve semantic source references through commissioned instance mappings. 3. Query the required historical range through `HistoryQuery`. 4. Read the latest values through the read-only `LiveState` port when the task allows a live tail and that feature uses `aggregation: last`, then replace only its final interval cell without changing SHM authority. Version 1 rejects live tail for `mean`, `sum`, `min`, or `max` because an instantaneous value cannot represent an aggregate bucket. The load/PV tasks forbid it. 5. Obtain future-known inputs through a typed `CovariateSource`. 6. Generate deterministic calendar features locally. 7. Verify exact commissioned unit/sign metadata, align timestamps, aggregate raw observations, resolve duplicates, and apply the declared missing-data policy. Version 1 performs no runtime unit/sign conversion. 8. Calculate frame quality and a canonical input digest. 9. Select the configured processor and submit one complete `ProcessingFrame`. 10. Validate the returned task ID, request ID, input digest, timestamps, units, status, model provenance, and expiry before exposing `DerivedData`. For interval-end cadence `c`, the historical labels are `as_of-(history_steps-1)c, ..., as_of`. A label `t` aggregates raw observations in `(t-c, t]`, and no source read may advance beyond `as_of`. Future covariates and forecast output start at `as_of+c`. The default runtime implements this against `aether-history.db` with the read-only SQLite adapter; all feature reads for a request share one transaction. The optional HTTP history adapter is only for an upstream service that already materializes the exact cadence grid and supports the restricted `last/reject` policy. Metadata checks do not prove interval meaning. Before enabling a physical route, use a site golden fixture to verify every raw source's aggregation and alignment semantics. This is mandatory for `rain`: `aggregation: sum` is valid only when each source value is incremental precipitation for its interval, not a rolling accumulation or rate. Nor does current SQLite history provide a point-in-time backtest cut. Rows have no ingestion timestamp or source/configuration epoch, so late backfills and physical remaps behind an unchanged logical series can alter or splice an old event-time window. Binding revisions validate the current route only. Offline evaluations need a frozen historian export captured at the evaluation cut, or a bitemporal, epoch-bearing history adapter. Freeze the artifact registry too: v1 artifact identity has version and digest but no `trained_through` or `available_at`, so an old `as_of` alone cannot exclude a model released later. Treat historian storage changes as maintenance. The `history_config.storage_*` rows are saved intent, and `PUT /hisApi/storage` does not reconnect the active writer. Disable processing, apply a reconnect or history-service restart, verify the active SQLite backend plus a commissioned sentinel series, and only then restart `aether-api` against the same path. Back this with filesystem isolation: give the API independently permissioned read-only access to the historian database/WAL/SHM directory, separate from its writable configuration/audit database. The base Compose `/app/data:rw` mount and SQLite read-only flags alone are not a production authority boundary. The processor gets no Aether database credentials and no reverse callback URL. If it needs another observation, the task declaration is incomplete and must be revised rather than allowing an undeclared read. ## Invoke the application API Version 1 is exposed by the opt-in, JWT-protected `aether-api` routes: - `GET /api/v1/data-processing/tasks`; - `GET /api/v1/data-processing/processors/health`; and - `POST /api/v1/data-processing/process`. Viewer, Engineer, and Admin roles may use discovery; processing requires Engineer or Admin. The process body is the strict application request, not a complete frame or processor endpoint. `x-request-id` is optional and `x-aether-confirmed: true` supplies explicit confirmation when route policy requires it. CLI and MCP bindings for these capabilities are not implemented in v1; future transports must call the same application API rather than the sidecar. ## Request a processor directly during development The optional HTTP adapter maps `DataProcessor::process` to the versioned processor-facing `POST /v1/process` endpoint. The repository's Load-Forecasting integration implements that endpoint; it is a processor boundary, not an application-facing endpoint on the default Aether services. ```bash curl --fail-with-body \ --request POST http://127.0.0.1:8989/v1/process \ --header 'Content-Type: application/vnd.aether.data-processing+json;version=1' \ --data @processing-request.json ``` Direct calls are useful for processor conformance tests. Production callers should enter through `DataProcessingApplication` so source resolution, data quality, policy, and result validation cannot be bypassed. ## Retry and content identity `data_processing.process` is a read-only query but is deliberately declared `idempotent: false`. Version 1 has no request replay store, de-duplication contract, exact-result guarantee, or `409 REQUEST_ID_REUSED` behavior. Each accepted invocation receives its own audit record and may execute the processor again. `input_digest` identifies the exact task/binding revision, frame, artifact selector, contract, and options. It supports correlation and offline comparison; it is not an idempotency key for the public operation. A caller may retry a typed retryable `429`, `503`, or `504` response only under its own bounded policy, with a fresh deadline and awareness that work may already have run. ## Conformance checklist Before routing a task to a processor, verify that it: - accepts a complete frame and makes no reverse data read; - validates every contract invariant in the reference page; - rejects NaN, infinity, timestamp disorder, unit mismatch, and undeclared features; - can reproduce a pinned frame in an offline golden test where the algorithm is deterministic, without treating that test property as an API replay promise; - does not call a current historian plus current model selection a point-in-time backtest; frozen history and artifact cuts, or equivalent bitemporal/availability metadata, are required; - handles timeout, disconnect, overload, and malformed responses as typed failures; - labels fallback output and never disguises unavailable data as a prediction; - returns model and processor provenance without local filesystem paths or secrets; - passes task-specific fixtures for normal, missing, stale, and boundary data; and - proves that processor loss cannot block acquisition or deterministic safety behavior. The current SQLite history schema does not retain device-origin sample quality, and the current SHM bridge labels accepted finite live values as `good`. Freshness, gaps, missingness, numeric constraints, issue time, and provenance are enforced, but a deployment requiring end-to-end source-quality fidelity must commission a quality-bearing source adapter before production. ## Related pages - [Data Processing Contracts](/en/reference/data-processing-contracts) — v1 wire contracts and validation rules - [HTTP Data Processor](/en/extensions/http-data-processor) — bounded local/remote adapter and composition API - [AetherEMS Power Forecasting](https://github.com/EvanL1/AetherEMS/blob/main/packs/energy/knowledge/power-forecasting.md) — the first downstream task and processor - [Load-Forecasting Processor](https://github.com/EvanL1/AetherEdge/blob/main/integrations/load-forecasting/README.md) — tested `/v1/process` adapter for the existing Edge-Platform - [JSON Schemas](https://github.com/EvanL1/AetherEdge/blob/main/contracts/data-processing/README.md) — strict v1 transport validation - [Data Flow](/en/concepts/data-flow) — authoritative live and historical paths - [System Architecture](/en/concepts/architecture) — core layers and service boundaries - [Safe Operations for Applications and Agents](/en/guides/safe-operations) — why derived data does not bypass control policy --- # Deployment Aether deploys as either a set of Docker containers or, for Docker-free targets, native systemd services. There are three paths: run the Docker Compose stack directly on a machine that can build images, package everything into a single self-extracting Docker-based installer and ship it to an edge device, or build a bare-metal installer that ships statically linked binaries and systemd units instead. ## Docker Compose ```bash cp .env.example .env # then edit: AETHER_BASE_PATH, HOST_UID/HOST_GID, RUST_LOG, ... docker compose up -d docker compose ps ``` The default Compose application starts only the six Rust services, all with `network_mode: host`. Redis and TimescaleDB start only when their explicit `redis` and `postgres-storage` profiles are selected. The base file exposes the forecast sidecar only through the mutable `data-processing-dev` profile; production requires the explicit override shown below. AetherEdge is a headless Kernel distribution. It does not build or install a browser client. The EMS operator console belongs to the independent [AetherEMS](https://github.com/EvanL1/AetherEMS) distribution and connects through the authenticated application API. | Container | Image | Role | |-----------|-------|------| | aether-redis | redis:8-alpine | Optional non-authoritative state mirror infrastructure (`redis` profile) | | aether-timescaledb | timescale/timescaledb:2.25.2-pg17 | Optional PostgreSQL history backend (`postgres-storage` profile) | | aether-load-forecasting-processor | operator-supplied, digest-pinned image | Optional request-driven processor (`data-processing` profile) | | aether-io | aetherems:latest | Communication service (privileged, mounts `/dev` for field buses) | | aether-automation | aetherems:latest | Model service and rule engine | | aether-history | aetherems:latest | SHM sampler with embedded SQLite history by default | | aether-api | aetherems:latest | REST API, WebSocket, JWT auth | | aether-uplink | aetherems:latest | MQTT cloud uplink, TLS certificates | | aether-alarm | aetherems:latest | Alarm rules and notifications | The production forecast profile requires an immutable image built from the existing Load-Forecasting service plus Aether's adapter, a commissioned artifact bundle, a matching bearer token, and a validated runtime YAML under `${AETHER_BASE_PATH}/config/data-processing/runtime.yaml`: Copy the repository's synthetic [`runtime.example.yaml`](https://github.com/EvanL1/AetherEdge/blob/main/packs/energy/data-processing/runtime.example.yaml) and [`covariates.example.json`](https://github.com/EvanL1/AetherEdge/blob/main/packs/energy/data-processing/covariates.example.json) into that deployment-owned directory, replace every logical/physical mapping, artifact digest, and covariate row, and validate them against the site database. The examples are not production values. ```bash export AETHER_LOAD_FORECASTING_IMAGE=registry.example/load-forecasting@sha256: export AETHER_LOAD_FORECASTING_BEARER_TOKEN='' export AETHER_LOAD_FORECASTING_ARTIFACT_BUNDLES='' integrations/load-forecasting/deploy/validate-production-env.sh docker compose \ -f docker-compose.yml \ -f integrations/load-forecasting/deploy/docker-compose.data-processing.yaml \ --profile data-processing \ up -d aether-load-forecasting-processor aether-api ``` The preflight is mandatory for this documented production path. It rejects a non-`@sha256` image reference, weak or malformed token, out-of-range concurrency, and non-strict artifact-bundle JSON before Compose evaluates the override. Historian authority requires a separate operational check. A storage `PUT /hisApi/storage` saves settings but does not reconnect the active writer. Keep Data Processing disabled across a storage change, reconnect or restart `aether-history`, verify its active SQLite backend and a commissioned sentinel series, then restart `aether-api` with runtime `history.path` matching the applied backend. The persisted `history_config.storage_*` rows alone do not prove the live write target. Direct SQLite history also needs a filesystem boundary. The API must receive the historian database, WAL, and SHM through a dedicated read-only directory mount (or an independently permissioned read-only OS account/ACL); its own `aether.db` and audit writes stay on a separate writable path. The base Compose currently mounts the whole `/app/data` directory read-write into `aether-api`, so the documented production Data Processing route is blocked until a site override provides and verifies that separation. SQLite `mode=ro` and `query_only=ON` alone are not sufficient containment. The `/api/v1/data-processing/process` call is non-idempotent and writes a mandatory audit record even when work is rejected. The current API does not provide an actor/IP request-rate limiter or an audit retention quota. A production ingress must therefore enforce authenticated actor and source-IP rates plus an in-flight ceiling, while operations monitor `command_audit_events` growth and apply a retention/export policy that preserves required evidence. Production enablement is blocked without those controls; the per-route processor semaphore alone does not bound rejected-call audit writes. It binds to loopback and receives no Aether data-directory, configuration, device, history-database, or SHM mount. The application sends a complete, bounded `ProcessingFrame` over the processor port. See [`../../integrations/load-forecasting/deploy/README.md`](https://github.com/EvanL1/AetherEdge/blob/main/integrations/load-forecasting/deploy/README.md) for the standalone systemd unit and commissioning requirements. The Compose sidecar joins only the dedicated `data-processing-local` network, which is declared `internal: true`; together with host-loopback publication, this mechanically blocks container external egress and limits inbound access. Native/systemd deployment still requires a host firewall or equivalent egress policy. The examples also leave CPU, memory, and PID quotas deployment-specific; set measured cgroup/systemd limits from a real artifact benchmark so processor load cannot starve deterministic services. The six Rust services share one `aetherems:latest` compatibility image, each started with its own command. The image name is retained while downstream release consumers migrate; it does not imply that this repository owns the EMS product or Console. Host networking does not make the unauthenticated process APIs public: IO, automation, history, uplink, and alarm bind only to `127.0.0.1`. Remote clients must enter through `aether-api` on port 6005, where JWT and role checks apply. The optional Redis and TimescaleDB listeners are also loopback-only. Two mount classes matter for the runtime: - **Shared memory and local event sockets** — the host's `/dev/shm` is bind-mounted at `/shm/rtdb` in all six Rust services. The mount is read-write because the SHM owner writes point slots while isolated consumers create their own subscription bitmaps and UDS endpoints beside the segment. Mounting the directory also avoids Docker auto-creating a stale file entry. - **Optional external stores** — no core service mounts a Redis socket, exports `REDIS_URL`, or waits for Redis. `docker compose --profile redis up -d` starts mirror infrastructure for a host that explicitly wires `aether-redis-bridge`. PostgreSQL history remains opt-in through `--profile postgres-storage` and a PostgreSQL-enabled history build. Set a unique non-empty `TIMESCALEDB_PASSWORD` before selecting that profile; the packaged extension installer generates one without printing it. All Rust containers read the shared configuration SQLite database from `${AETHER_BASE_PATH:-./data}/aether.db` (mounted at `/app/data/aether.db`) and write logs to `${AETHER_LOG_PATH:-./logs}`. aether-history stores samples in `/app/data/aether-history.db` unless a PostgreSQL-enabled build and backend configuration are explicitly selected. The services remain six independent processes. SHM/UDS replaces a mandatory live-data broker; it does not collapse their restart or fault-isolation boundaries. ## Edge installer `scripts/build-installer.sh` produces a single self-extracting `.run` file containing everything an offline edge device needs — Docker image archives, the compose file, configuration templates, the `aether` CLI binary, and an install script: ```text ./scripts/build-installer.sh [VERSION] [ARCH] [TARGET] [--services=...] [--enable-swagger] ``` - `VERSION` — version string, defaults to today's date (`YYYYMMDD`) - `ARCH` — `arm64` (default) or `amd64` - `TARGET` — Rust target triple; defaults to `aarch64-unknown-linux-musl` for arm64 and `x86_64-unknown-linux-musl` for amd64 - `--services` / `-s` — comma-separated subset to include (service names: `aether-io`, `aether-automation`, `aether-history`, `aether-api`, `aether-uplink`, `aether-alarm`, `redis`, `timescaledb`; group shortcut `rust` expands to all six Rust services). Every fresh-install package must include the Rust core; select extension variants as `-s rust,redis`, `-s rust,timescaledb`, or `-s rust,redis,timescaledb`. The default package contains only the Rust edge-runtime image; external-store images must be selected explicitly. - `--enable-swagger` — compile the Rust services with their feature-gated Swagger UI enabled ```bash # Full installer for an ARM64 edge device ./scripts/build-installer.sh # All Rust services only, with Swagger UI ./scripts/build-installer.sh v1.2.0 arm64 -s rust --enable-swagger ``` The script cross-compiles the six services and the `aether` CLI with `cargo zigbuild` for the target triple, builds the `aetherems` Docker image from those binaries, saves the images with `docker save` (plus the Redis and TimescaleDB images when selected), and packages the result with `makeself` into `release/AetherEdge--.run` (subset builds via `--services` append a service-list suffix to the file name, and `--enable-swagger` appends `-swagger`). The build host needs Docker, `cargo-zigbuild` (auto-installed via `cargo install` if missing), and `makeself` (auto-installed via Homebrew on macOS). Ship and run: ```bash scp release/AetherEdge-arm64-.run root@192.168.30.21:/tmp/ ssh root@192.168.30.21 'chmod +x /tmp/AetherEdge-arm64-.run && /tmp/AetherEdge-arm64-.run' ``` The embedded installer supports a **fresh deployment only**. Its first step is a read-only preflight: if it finds an Aether installation root, install context, site configuration or database, Aether container, or Aether systemd unit, it exits before stopping a service, loading an image, or writing a file. On an accepted clean host it installs to `/opt/AetherEdge`, loads the bundled images with `docker load`, activates the fail-safe template at `/opt/AetherEdge/data/config`, records the layout in `/etc/aether/install.yaml`, initializes a new database, and starts the six containers with Docker Compose. The deployment is Docker-based — the installer delivers images and compose configuration, not standalone service binaries. In-place upgrade, rollback to an older release, and import of an old database or installation layout are not supported in this release. To replace an installation, first export and back up anything that must be retained, run the deployment-specific uninstall procedure, and manually relocate or remove every retained Aether footprint before invoking the new installer. Translating retained data into a new release is currently an operator-managed migration outside the installer; do not point a fresh installer at an old site directory. `/opt/AetherEdge` is intentionally fixed for this release because packaged service-management paths assume that composition root. The installer rejects `AETHER_INSTALL_DIR` overrides rather than completing an installation whose later lifecycle operations would target a different root. `AETHER_BASE_PATH` may place a **new, empty** data/configuration tree in a dedicated child directory on another disk, but it must be chosen before installation and is not a migration switch. The installer rejects `/`, system roots, generic mount roots, symlinked paths, the installation root, and any destination containing an Aether site before any recursive permission operation. Paths are also limited to characters that round-trip safely through Docker Compose `.env`. An `AETHER_TIMESCALE_DATA_PATH` outside the site root and Docker's optional `redis-data` named volume are extension-owned storage. They must also be empty for a fresh deployment. Reusing or migrating an extension store is outside the installer's supported workflow. The installer generates `AETHER_BOOTSTRAP_ADMIN_PASSWORD`, persists it only in the mode-0600 `.env`, and never prints the value. The completion message provides a local retrieval command. Sign in as `admin`, change the password immediately, then remove the bootstrap variable. Anonymous registration remains disabled unless `AETHER_ALLOW_PUBLIC_REGISTRATION=true` is explicitly set. The API container runs as `HOST_UID:HOST_GID`. It has neither the Docker socket nor the installation root mounted, and `/etc/systemd/network` is read-only. Consequently host-network mutation requests fail closed. Remote runtime upgrade is not supported; installing another release requires the explicit fresh-deployment workflow above rather than expanding the API process's authority. ## Pack-only artifact A domain Pack is released separately from the fresh-install `.run` package. A Pack bundle contains only `pack-artifact.json` and the declarative `pack/` tree—never the `aether` CLI, a service binary, or a core crate. Build it from the exact runtime manifest generated for the target Kernel composition: ```bash ./scripts/build-pack-artifact.sh \ packs/ \ build/installer/runtime/runtime-manifest.json \ release/.bundle ``` Copy that directory to an edge host which already has the matching Kernel, then install it with the host's CLI: ```bash aether packs install --artifact /tmp/.bundle ``` The command refuses a different Kernel version, target triple, or complete runtime-manifest digest. It also rejects extra top-level entries, symlinks, executables/source trees, payload tampering, unbounded files, and an incompatible `pack.yaml`. After verification it publishes the data below the installed data directory as `packs//` and replaces `global.yaml` atomically only after validating the complete candidate active Pack set. A failed activation preserves the previous configuration and removes the newly published version. This command does not restart services or commission the Pack. Plan any maintenance restart separately, then run `aether doctor`; enabling channels, instances, rules, processors, or physical control remains a distinct audited commissioning action. The repository can build and test this local format, but does not yet claim an independently published/signed Kernel artifact, Pack artifact, or downstream second-repository release gate. ## Bare-metal Linux (systemd) For edge devices that cannot or should not run Docker, `scripts/build-installer.sh --bare-metal` produces a second kind of `.run` package: a self-contained bundle of statically linked binaries and systemd units, with zero container runtime dependency on the target machine. It contains the six Rust services, the `aether` CLI, and the core systemd units. Static `redis-server`/`redis-cli` and their unit are included only when Redis is selected. `scripts/build-static-deps.sh` uses `INCLUDE_REDIS=1` for that extension bundle. The core services are grouped by `aether.target`. The pinned Redis release also pins its source-archive SHA-256 value. Overriding the version requires its matching `REDIS_SHA256`; a cached binary is reused only with a matching provenance marker and after its static ELF linkage and target architecture are checked. The bare-metal runtime root is likewise fixed at `/opt/aether`, matching the packaged systemd units. `AETHER_INSTALL_DIR` overrides are rejected. Its bootstrap administrator credential is stored in `/etc/aether/aether.env` (mode 0600) with the same retrieve-change-remove lifecycle as Docker. Build: ```bash # Core-only package (default) ./scripts/build-installer.sh --bare-metal [VERSION] [ARCH] # Core plus optional Redis mirror infrastructure ./scripts/build-installer.sh --bare-metal [VERSION] [ARCH] -s rust,redis ``` This follows the same `[VERSION] [ARCH] [TARGET]` positional convention as the Docker build — `--bare-metal` is an added flag, order of the other arguments is unchanged. It cross-compiles the same six services plus the `aether` CLI and packages them with `makeself` into `release/AetherEdge-baremetal--.run`. Selecting Redis adds `-redis` to the file name. A bare-metal package must include the Rust core. TimescaleDB is an external bare-metal extension and is not bundled by this builder. Ship and run as root — the installer refuses to proceed without `systemctl` on PATH: ```bash scp release/AetherEdge-baremetal-arm64-.run root@192.168.30.21:/tmp/ ssh root@192.168.30.21 'chmod +x /tmp/AetherEdge-baremetal-arm64-.run && /tmp/AetherEdge-baremetal-arm64-.run' ``` `scripts/install-baremetal.sh` (the script the `.run` archive extracts and runs) lays out the install as: | Path | Contents | |------|----------| | `/opt/aether/bin/` | Service binaries and `aether` CLI; Redis tools only in an explicitly selected extension bundle | | `/etc/aether/config/` | The activated configuration (from `config.template/` on first install) | | `/etc/aether/aether.env` | Explicit config/data/database paths, `AETHER_LOG_DIR`, `RUST_LOG`, and freshly generated secrets (mode 600) | | `/etc/aether/install.yaml` | Non-secret installed layout used by the CLI (`config_dir`, `data_dir`, runtime mode, release channel, and enabled packs) | | `/etc/aether/script-host/main.py` | The Python script host for aether-io custom transforms (matches the deployed-path lookup in `services/io/src/protocols/core/script_runner.rs`) | | `/var/lib/aether/` | Service logs (`logs/`) and optional Redis data (`redis/`) | It also symlinks `aether` onto `/usr/local/bin` and drops a `/etc/profile.d/aether.sh` PATH entry, installs the systemd units, runs `aether init` and `aether sync` against `/etc/aether/config`, and finishes with `systemctl enable --now aether.target`. Day-to-day operation is native systemd: ```bash systemctl status aether.target journalctl -u aether-io -f ``` `aether services` and `aether doctor` auto-detect this mode — see [CLI Reference: aether services](/en/reference/cli#aether-services) and [aether doctor](/en/reference/cli#aether-doctor) — with no flag needed. Detection (`tools/aether/src/deploy_mode.rs`) checks for both `/etc/systemd/system/aether.target` and `systemctl` on PATH; if either is missing it falls back to the Docker Compose code path. In systemd mode, `aether services start/stop/restart/status` pass canonical service names such as `aether-io` directly to `systemctl ` (or use `aether.target` when no service is named), and `aether services logs ` shells out to `journalctl -u `. `aether services build/pull/clean` all error in this mode — there are no container images in a bare-metal install, and the `.run` package is not an in-place upgrader. `aether services refresh --smart` degrades to a plain `systemctl restart`, printing a note that `--smart` has no effect, since there's no image to diff against. Redis is not part of the default health contract; operators who enable the extension can inspect its unit or profile independently. None of the six Rust service units declares `Requires=aether-redis.service`. The default target starts and keeps its SHM/SQLite work independently; an enabled Redis mirror cannot become a service-availability dependency. The bare-metal installer has the same fresh-only contract as the Docker installer. Re-running a `.run` package on a host with `/opt/aether`, `/etc/aether`, `/var/lib/aether`, installed units, or runtime data fails during read-only preflight, before `aether.target` is stopped or files are replaced. There is no automatic binary replacement, configuration merge, optional-unit migration, or previous-release rollback path. Back up/export required state, uninstall the old runtime, and manually relocate every retained footprint before installing a new release; importing that state into the new release is not currently supported by the installer. This does not remove the installer's failure cleanup for a partially completed fresh installation. Uninstall with the script the installer writes: ```bash /opt/aether/uninstall.sh ``` It stops and disables `aether.target`, removes the systemd units, the `aether` symlink, the PATH entry, and `/opt/aether` itself. `/etc/aether` and `/var/lib/aether` (configuration and runtime data) are left in place. Those retained directories intentionally make a later fresh install fail until an operator has exported, relocated, or removed them. ## Runtime paths The shared-memory segment path is resolved in this order (`crates/aether-dataplane/src/core/config.rs`): 1. `AETHER_SHM_PATH` environment variable, if set 2. `/shm/rtdb/aether-rtdb.shm`, if the `/shm/rtdb` directory exists (the Docker mount point) 3. `/dev/shm/aether-rtdb.shm` on Linux 4. `/tmp/aether-rtdb.shm` elsewhere (macOS development) Inside containers, `/shm/rtdb` is the host's `/dev/shm`, so both views name the same file. Docker also places the aether-automation command socket and PointWatch socket in this directory through `AETHER_M2C_SOCKET` and `AETHER_AUTOMATION_POINT_WATCH_SOCKET`; native deployments keep the `/tmp` defaults. Peripheral PointWatch socket names are derived from the resolved SHM path, so each process binds a distinct endpoint. Other state: - **SQLite** — `aether.db` lives in the data directory: `/opt/AetherEdge/data` on an installed device, `./data` in a compose checkout (`AETHER_BASE_PATH`); containers see it as `/app/data/aether.db` (`AETHER_DB_PATH`). - **Embedded history** — aether-history writes `aether-history.db` in the same data directory by default (`AETHER_HISTORY_DB_PATH`). PostgreSQL/TimescaleDB is an opt-in storage adapter, not a base-runtime prerequisite. - **Configuration** — the `aether` CLI first honors flags and `AETHER_*_PATH` overrides, then reads `/etc/aether/install.yaml`. Without an install context, a source checkout uses `./data/config` and `./data`; an unregistered old installation directory is never adopted implicitly. - **Logs** — `${AETHER_LOG_PATH:-./logs}` on the host, `/app/logs` in the containers. ## Service management on device The installed `aether` CLI wraps Docker Compose for day-to-day operations: ```bash aether services start # start one or more services (or all) aether services stop # stop services aether services status # container status aether services refresh # recreate containers from the installed composition aether services logs # view service logs aether doctor # Docker, core services, SQLite, config files, # shared memory ``` `aether services refresh` recreates containers from the composition and image set already installed on the device. It is a same-release recovery operation, not a supported path for replacing the installed release. See [Getting Started](/en/guides/getting-started) for what a healthy `aether doctor` run covers. ## Related pages - [Getting Started](/en/guides/getting-started) — build, initialize, and verify a fresh checkout - [Connect Devices](/en/guides/connect-devices) — add channels and map points once the stack is running - [System Architecture](/en/concepts/architecture) — the services these containers run --- # Getting Started This guide takes you from a fresh clone to a running, uncommissioned Aether system: build the `aether` CLI, apply a reviewed safe-empty setup plan, start the services, and confirm that the runtime is healthy. ## Prerequisites - **Rust** — the toolchain is pinned to `1.90.0` by `rust-toolchain.toml`; rustup installs it automatically on first build. The pin also declares the `aarch64-unknown-linux-musl` cross-compilation target used for edge builds. - **Docker Engine and Docker Compose** — required for the container composition. `aether services start` drives Docker Compose under the hood. Redis and PostgreSQL are not prerequisites. ## Build and configure Build the `aether` CLI: ```bash cargo build --release -p aether ``` Install the binary onto your PATH — `cp target/release/aether /usr/local/bin/` or `cargo install --path tools/aether` — so this and every other guide can invoke it as bare `aether`. The repository ships a fail-safe empty configuration in `config.template/`. In a source checkout the CLI and `docker-compose.yml` both use `./data/config` and `./data` by default. Planning is persistently read-only and will not create either directory: ```bash aether --json setup ``` Read `data.plan_id` from the JSON output, review the listed actions, then explicitly apply that exact unchanged plan: ```bash aether setup apply --plan-id ``` Apply is accepted only for a fresh site or an exact safe subset of the four distribution files. Before any persistent write, Aether stages the complete configuration, runs normal validation and the full atomic sync against a temporary SQLite database, then creates only missing files without overwriting. It initializes `aether.db` and syncs the empty runtime, but does not start a service, enable a device or rule, or install a domain pack. If the site changed after planning, the plan ID is stale and apply stops without a write. Rerunning setup on the resulting `safe_ready` site is a no-op. Existing/custom sites are reported but never rewritten by setup. Operators can still use `aether init` for an explicit schema migration and `aether sync` for an explicit configuration apply; `aether sync --dry-run` validates the same nested files without changing the installed database. The CLI resolves each path independently in this order: command-line flag, `AETHER_CONFIG_PATH`/`AETHER_DATA_PATH`, `/etc/aether/install.yaml`, then the current checkout's `data/config/` and `data/`. Installed packages write the context file automatically. Without that context, Aether never adopts an old installation directory merely because it exists. For a fresh manual Compose deployment, create a private environment file and fill both first-start secrets before validating the composition. Packaged installers do this automatically; repository setup deliberately keeps secrets out of configuration templates. ```bash cp .env.example .env chmod 600 .env random_hex_32() { if command -v openssl >/dev/null 2>&1; then openssl rand -hex 32 else od -An -N32 -tx1 /dev/urandom | tr -d ' \n' fi } export JWT_SECRET_KEY="$(random_hex_32)" export AETHER_BOOTSTRAP_ADMIN_PASSWORD="$(random_hex_32)" env_tmp="$(mktemp ./.env.tmp.XXXXXX)" chmod 600 "$env_tmp" awk ' /^JWT_SECRET_KEY=/ { print "JWT_SECRET_KEY=" ENVIRON["JWT_SECRET_KEY"]; next } /^AETHER_BOOTSTRAP_ADMIN_PASSWORD=/ { print "AETHER_BOOTSTRAP_ADMIN_PASSWORD=" ENVIRON["AETHER_BOOTSTRAP_ADMIN_PASSWORD"]; next } { print } ' .env > "$env_tmp" mv "$env_tmp" .env JWT_SECRET_KEY="$JWT_SECRET_KEY" \ AETHER_BOOTSTRAP_ADMIN_PASSWORD="$AETHER_BOOTSTRAP_ADMIN_PASSWORD" \ docker compose config --quiet unset JWT_SECRET_KEY AETHER_BOOTSTRAP_ADMIN_PASSWORD ``` Keep `JWT_SECRET_KEY` stable. Sign in as `admin` with the generated bootstrap value, change the password immediately, then remove `AETHER_BOOTSTRAP_ADMIN_PASSWORD` from `.env`. Public registration stays off because the example sets `AETHER_ALLOW_PUBLIC_REGISTRATION=false`. ## Start and verify ```bash aether services start aether doctor ``` `aether services start` brings up the Docker Compose stack. The compose file references pre-built images; on a machine that does not yet have `aetherems:latest`, produce it by running `./scripts/build-installer.sh` (which builds the image from cross-compiled binaries) or load a prebuilt image archive with `docker load` — see [Deployment](/en/guides/deployment). `aether doctor` checks the required local runtime and exits nonzero if any required component fails: 1. **Docker Engine** — the daemon is installed and running. 2. **Six core services** — IO, automation, history, API, uplink, and alarm answer their service-specific health routes. Optional cloud or storage dependencies may report degraded without becoming core failures. 3. **SQLite database** — `aether.db` exists, is initialized, and shows its last sync time. 4. **Config files** — `global.yaml`, `io/io.yaml`, `automation/automation.yaml`, and `automation/instances.yaml` are present. 5. **Shared memory** — the segment file `/dev/shm/aether-rtdb.shm` exists and has a readable, valid data-plane header and a fresh IO-writer heartbeat. Missing, stale, truncated, symlinked, or invalid SHM is an error because it is the authoritative live-state plane. `AETHER_SHM_PATH` overrides the platform default when an installation deliberately uses another location. With everything healthy, these ports are listening (see [System Architecture](/en/concepts/architecture) for what each service does). Only the authenticated API gateway is remotely exposed by the packaged composition; the other five process APIs listen on `127.0.0.1`: | Service | Port | |---------|------| | aether-io | 6001 | | aether-automation | 6002 | | aether-history | 6004 | | aether-api | 6005 | | aether-uplink | 6006 | | aether-alarm | 6007 | AetherEdge intentionally exposes no bundled Web UI. Product consoles such as AetherEMS are deployed independently and enter through `aether-api`. ## First look around The default template deliberately contains no device channel or instance, so these commands should initially return empty collections: ```bash # 1. The communication channels aether-io is polling aether channels list # 2. The device instances aether-automation is serving aether models instances list # 3. Confirm that no control rule was activated implicitly aether rules list ``` Every command accepts `--json` for structured output, which is the mode AI agents and scripts should use. Data starts flowing only after an explicit commissioning step adds and enables a channel; continue with Connect Devices. ## Next steps - [Connect Devices](/en/guides/connect-devices) — add a real channel and map its points to instances - [Writing Rules](/en/guides/writing-rules) — automate control with the rule engine - [AI Assistants](/en/guides/ai-assistants) — drive Aether from an AI agent - [Deployment](/en/guides/deployment) — Docker Compose details and the edge installer --- # Safe Operations for Applications and Agents AetherEdge treats every browser, CLI, AI assistant, and generated backend as an untrusted application client. Read operations and commands use the same application boundary, but commands additionally require an authenticated actor, an explicit permission, the declared confirmation policy, and durable audit evidence. Access to an HTTP route or MCP tool is never device authority. ## Use the public application boundary Remote clients connect only to authenticated `aether-api:6005` routes and the gateway WebSocket. IO, automation, history, uplink, and alarm process ports are internal loopback interfaces. Do not expose them, proxy them from a product UI, or bypass the application API by writing SHM, SQLite, Pack files, or an external mirror. Live point state comes from SHM. A database, cache, history adapter, browser store, or AI context may be delayed and must not silently replace that authority. Preserve topology epoch, source timestamp, freshness, health, and configuration revision whenever the contract supplies them. ## Start read-only - MCP omits write tools unless `--allow-write` is deliberately selected. - Generated applications should omit command controls until the use case and permissions are explicit. - Device control is deny-by-default. A Viewer token never becomes a command identity because the client sends a role or actor header. - The gateway forwards signed credentials and confirmation but strips caller-supplied actor identity headers. Each downstream application service independently verifies the signed identity. ## Treat command metadata as policy Before issuing a command, verify its current OpenAPI or MCP metadata: - query or command classification; - risk level and required permission; - idempotency and retry policy; - confirmation requirement; - expected revision or topology fence; - audit and correlation fields. Send `confirmed: true` or `x-aether-confirmed: true` only after the operator has confirmed the concrete target and effect. Preserve `request_id`, `command_id`, and resulting revision values in logs and UI state. ## Handle uncertain outcomes Do not automatically retry a non-idempotent command after timeout, an accepted response with incomplete audit, a degraded runtime projection, or a lost connection. The durable change may already exist. Query the command receipt, audit record, configuration revision, and reconciliation state first. Command acceptance means the local command plane accepted the request. It does not prove physical execution. Use feedback telemetry, with a matching topology generation and freshness policy, before reporting a closed-loop result. ## Keep deterministic safety independent of AI AI may propose, explain, or invoke governed application capabilities. It must not participate in protocol polling, acquisition timing, SHM publication, safety interlocks, or hard real-time control. The edge runtime and commissioned rules must remain deterministic when every AI client is disconnected. Energy-specific limits, equipment semantics, and commissioning policy belong to the active domain Pack. For the official energy implementation, read the [AetherEMS Energy Pack safety guide](https://github.com/EvanL1/AetherEMS/blob/main/packs/energy/knowledge/safe-operations.md). ## Related pages - [Build Applications with AI](/en/guides/build-applications-with-ai) - [Connect AI Assistants](/en/guides/ai-assistants) - [HTTP API reference](/en/reference/http-api) - [MCP tool reference](/en/reference/mcp-tools) - [Shared memory](/en/concepts/shared-memory) --- # Writing Rules Rules are Aether's control logic: flows that read measurement points (M), evaluate conditions, and write action points (A). They execute inside automation (port 6002) and are authored through the application API, either directly or through a downstream product console. This guide covers the authoring mechanics; for how the engine schedules and executes rules, see [Rule Engine](/en/concepts/rule-engine), and for a worked control strategy, see [Control Strategies](https://github.com/EvanL1/AetherEdge/blob/main/docs/domain/control-strategies.md). ## Anatomy of a rule A rule row in the SQLite `rules` table carries: - **`id`** — an auto-assigned integer; you never choose it. - **`name`** and **`description`** — for humans. - **`enabled`** — new rules start disabled; the scheduler skips disabled rules entirely. - **`priority`** — orders evaluation when several rules are due; see [Control Strategies](https://github.com/EvanL1/AetherEdge/blob/main/docs/domain/control-strategies.md) for how priority combines with mutually exclusive conditions to arbitrate between rules that write the same actuator. - **`cooldown_ms`** — a minimum gap after a successful execution that performed at least one action, suppressing re-execution until it elapses. - **`trigger_config`** — when the rule runs. Two variants, discriminated by `"type"`: - `{"type": "interval", "interval_ms": 1000}` — periodic evaluation on the scheduler tick. Rules with no `trigger_config` default to a 1000 ms interval (or to their `cooldown_ms` as the period, if set). - `{"type": "on_change", "point_refs": [{"instance": 1, "point_type": "measurement", "point": 0}], "time_deadband_ms": 200, "value_deadband": null}` — event-driven evaluation when a subscribed point changes, filtered by a time deadband (minimum gap between triggers) and an optional value deadband (absolute or percent change threshold). - **The flow** — the logic itself: a start node fanning out to input nodes (read a measurement point or load configuration parameters), through decision nodes (condition branches), to action nodes (write an action point), ending at an end node. The flow is stored twice — `flow_json`, the full visual-editor document, and `nodes_json`, the compact topology the engine executes — and the two columns are always derived together from the editor document by one function. [Rule Engine](/en/concepts/rule-engine) explains why. ## Via a downstream application The independent [AetherEMS](https://github.com/EvanL1/AetherEMS) Console is an optional energy-domain reference application with a Vue Flow rule editor. It edits the complete visual document — nodes with canvas positions, labels, edges, and viewport — and submits that document through the same authenticated rule command API. AetherEdge does not bundle the Console or grant it direct SQLite/SHM access. The server derives both stored representations together, so `flow_json` and the execution topology cannot drift (see [Rule Engine](/en/concepts/rule-engine) for the invariant). ## Via the HTTP API automation serves the rule API (`services/automation/src/rule_routes.rs`): Swagger UI at `http://localhost:6002/docs` is the per-operation contract source. Every mutation below accepts only a Bearer Admin/Engineer actor, requires `confirmed: true`, and writes mandatory audit records before changing SQLite or reloading the scheduler. | Method | Path | Purpose | |--------|------|---------| | GET | `/api/rules` | Paginated list (summary fields: id, name, enabled, description) | | POST | `/api/rules` | Create a metadata-only stub | | GET | `/api/rules/{id}` | Full rule, including both flow columns | | PUT | `/api/rules/{id}` | Partial update; the flow and trigger land here | | DELETE | `/api/rules/{id}` | Delete the rule | | POST | `/api/rules/{id}/enable` | Set enabled | | POST | `/api/rules/{id}/disable` | Set disabled | | POST | `/api/rules/{id}/execute` | Execute immediately (real execution — see below) | | GET | `/api/rules/{id}/variables` | Variables the rule reads, for monitoring | | GET | `/api/scheduler/status` | Scheduler running flag, rule counts, tick interval | | POST | `/api/scheduler/reload` | Force re-read of all rules from SQLite | Creating a rule is a **two-step reality**: `POST /api/rules` accepts only a name and description and inserts a stub — empty `{}` topology, no editor document, disabled. The flow content only lands via `PUT /api/rules/{id}`: ```bash # 1. Create the stub; the response carries the assigned id curl -X POST http://localhost:6002/api/rules \ -H "Authorization: Bearer $AETHER_ACCESS_TOKEN" \ -H 'Content-Type: application/json' \ -d '{"name": "Battery SOC Protection", "description": "Protect battery when SOC is too low", "confirmed": true}' # → {"success": true, "data": {"id": 3, "name": "Battery SOC Protection", "status": "created"}} # 2. Write the flow and trigger curl -X PUT http://localhost:6002/api/rules/3 \ -H "Authorization: Bearer $AETHER_ACCESS_TOKEN" \ -H 'Content-Type: application/json' \ -d @rule.json # 3. Enable it curl -X POST http://localhost:6002/api/rules/3/enable \ -H "Authorization: Bearer $AETHER_ACCESS_TOKEN" \ -H 'Content-Type: application/json' \ -d '{"confirmed": true}' ``` where `rule.json` supplies the editor document and trigger: ```json { "flow_json": { "nodes": [ {"id": "start", "type": "start", "position": {"x": 0, "y": 0}, "data": {"config": {"wires": {"default": ["end"]}}}}, {"id": "end", "type": "end", "position": {"x": 100, "y": 0}} ], "edges": [] }, "trigger_config": {"type": "interval", "interval_ms": 1000}, "confirmed": true } ``` That flow is the minimal valid document (it does nothing); for a full strategy with input, decision, and action nodes, see the shipped template `packs/energy/rules/battery_soc_management.json`, which [Control Strategies](https://github.com/EvanL1/AetherEdge/blob/main/docs/domain/control-strategies.md) walks through node by node. A malformed flow fails the PUT as a unit — nothing is stored — and a malformed `trigger_config` is rejected at the same boundary. The `aether` CLI wraps the same endpoints. Set `AETHER_ACCESS_TOKEN` and pass `--confirmed` to `rules create`, `update`, `enable`, `disable`, and `delete`; `delete --force` only skips the interactive prompt. `rules list` remains read-only. ## Testing a rule **There is no dry-run.** `POST /api/rules/{id}/execute` — and equally the `rules_execute` MCP tool and `aether rules execute --confirmed` — performs a real execution through the authenticated, confirmed, and audited application command: the flow is evaluated against live values, and any action that fires is submitted to the local command plane. Acceptance does not prove that the physical device executed the command or reached the target value. So test against hardware that does not exist yet. The Virtual protocol has no feature gate precisely so it is always available for this: 1. Create a Virtual-protocol channel with control and adjustment points matching what the rule will write, and route a scratch instance's action points to it (see [Connect Devices](/en/guides/connect-devices)). 2. Point the rule's actions at the scratch instance and execute: ```bash AETHER_ACCESS_TOKEN='' \ aether rules execute 3 --confirmed ``` 3. Check the result. The command response reports `actions_attempted` and `actions_succeeded`, where success means local command-plane acceptance. Read back the corresponding measurements to verify physical behavior. The detailed execution path and action outcomes remain persisted locally in SQLite `rule_history` for API and WebSocket readers. 4. Once the branch selection and written values look right, re-target the rule's actions at the production instance and enable it. ## Reload You normally never think about reloading: all five rule CRUD endpoints — create, update, delete, enable, disable — trigger a scheduler reload after their database write, so changes made through the API or the editor take effect immediately, with no service restart. The explicit `POST /api/scheduler/reload` exists for out-of-band writes: a bulk import or `aether sync` pushing rule files into SQLite behind the scheduler's back. After such a write, hit the endpoint once and the scheduler re-reads every enabled rule and rebuilds its on-change subscriptions atomically. `GET /api/scheduler/status` confirms the result — `running`, total and enabled rule counts, and the tick interval. ## Related pages - [Rule Engine](/en/concepts/rule-engine) — dual-column storage, scheduling, execution, hot reload - [Control Strategies as Rules](https://github.com/EvanL1/AetherEdge/blob/main/docs/domain/control-strategies.md) — expressing SOC management and peak shaving as flows - [Connect Devices](/en/guides/connect-devices) — channels, Virtual protocol, point mapping --- # AetherIoT AetherIoT is the open-source project for AetherEdge, AetherCloud, and AetherContracts. AetherEMS is an energy-management solution built on the platform rather than part of its industry-neutral core. Start with the product that owns your task: - [AetherEdge](/en/aetheredge/) runs acquisition, deterministic behavior, local history, and final physical execution. - [AetherCloud](/en/aethercloud/) coordinates desired state, governed jobs, and provider-native cloud resources. - [AetherContracts](/en/aethercontracts/) defines the shared protocol, Schemas, fixtures, and TCK. - Understand boundaries at [Platform Overview](/en/overview/platform/). - Follow the complete [Edge to Contracts to Cloud tutorial](/en/tutorials/edge-contracts-cloud/). - Select tested versions in [Compatibility](/en/compatibility/version-matrix/). - Distinguish shipped and planned work at [Status and Roadmap](/en/roadmap/status/). - Install the edge runtime at [Agent Quickstart](/en/agent-quickstart/). - Discover every published document through [`/en/llms.txt`](https://docs.aetheriot.workers.dev/en/llms.txt). - Fetch the complete corpus through [`/en/llms-full.txt`](https://docs.aetheriot.workers.dev/en/llms-full.txt). Browsers receive the rendered documentation site. Agents can append `.md` to any document URL or send `Accept: text/markdown` to retrieve the Markdown source directly. Product repositories remain authoritative for implementation details, and AetherContracts remains the sole shared protocol authority. --- # Migrate from AetherIot to AetherEdge The edge product and repository are renamed from AetherIot to AetherEdge. AetherIoT becomes the umbrella project name for AetherEdge, AetherCloud, and AetherContracts. This is a product-identity change, not a protocol or package namespace rewrite. ## What changes - Repository URL: `https://github.com/EvanL1/AetherEdge`. - Source checkout directory and new source archive prefix: `AetherEdge`. - Product-facing documentation, badges, release links, CI examples, and website navigation use AetherEdge. - AetherCloud and future AetherContracts documentation refer to the edge product as AetherEdge. ## What stays stable - The `aether` CLI and `aether-*` binaries. - Rust crate and import names, including `aether-edge-sdk` and `aether_sdk`. - Configuration keys, environment variables, service identities, and on-disk paths unless a separate compatibility decision changes them. - Installer name `AetherEdge--.run`. - CloudLink, Thing Model, Schema, TCK, and failure-code identifiers. - Published tags, release assets, attestations, and digest-pinned AetherContracts alpha.3 artifacts. ## Update an existing clone After the GitHub repository rename: ```bash git remote set-url origin https://github.com/EvanL1/AetherEdge.git git remote -v ``` Existing GitHub redirects are a transition aid, not the permanent configuration. Update Git dependencies, submodules, badges, release automation, attestation commands, and allowlists to the new URL. ## Maintainer rollout checklist 1. Publish the product-family overview, unified documentation structure, ADR, compatibility matrix, status page, and this migration guide. 2. Update product-facing references while excluding immutable releases, evidence, provenance records, and digest-pinned contract imports. 3. Verify AetherEdge docs and release workflows, AetherCloud docs, the website, and AetherContracts migration notice. 4. Rename `EvanL1/AetherIot` to `EvanL1/AetherEdge` on GitHub. 5. Update local remotes, repository descriptions, website links, default-branch references, release badges, and attestation examples. 6. Re-run checks and inspect open pull requests and release links through the new address. 7. Announce a compatibility window and retain the old-name explanation until downstream references no longer depend on redirects. ## Rollback If a release, CI, package consumer, or documentation deployment cannot resolve the new repository identity, pause the rollout and restore the former GitHub repository name. Keep the AetherIoT product-family documentation and the stable software identifiers; revert only repository-facing URLs and source archive examples. Never rewrite published artifacts to simulate a rollback. --- # Deployment topologies AetherIoT supports deployments that keep physical authority at the edge while adding cloud coordination only where it creates value. ## Edge-only site ```text field devices -> AetherEdge -> local applications | `-> embedded history and durable local outbox ``` Use this topology for evaluation, isolated sites, and applications that do not need cloud coordination. The default runtime requires no Broker, PostgreSQL, cloud account, browser, or AI client. ## Edge with AetherCloud ```text field devices -> AetherEdge == CloudLink ==> AetherCloud -> operator clients ^ | | `-> provider adapters `--- final local policy and governed jobs ``` AetherContracts defines the shared CloudLink behavior. AetherCloud records a time-stamped projection and sends desired state or governed jobs. AetherEdge validates, accepts, rejects, expires, or applies that intent under local policy. The current CloudLink alpha path is experimental. The legacy edge uplink remains the default until authentication, signed acknowledgement, crash durability, and joint conformance gates pass. ## Multi-cloud control-plane cell ```text tenant home cell ├── AetherCloud application modules ├── PostgreSQL transactional state ├── encrypted artifact storage └── capability-driven provider adapters ├── provider A └── provider B ``` One deployment stack owns one independently locked infrastructure state. A cell does not create a tenant-wide or cross-provider global state file. Each provider continues to own its native resource state. ## AetherEMS solution AetherEMS layers energy models, workflows, and applications over AetherEdge and may connect to AetherCloud through public contracts. It cannot change the edge, cloud, or contract authority boundaries. ## Failure expectations | Failure | Required behavior | | --- | --- | | Internet or cloud unavailable | AetherEdge continues commissioned acquisition, local history, rules, alarms, interlocks, and control | | Broker unavailable | Local durable outbox retains bounded work; MQTT delivery is not an application receipt | | Provider API unavailable | AetherCloud exposes a typed failure and never normalizes it to an empty successful observation | | AI client unavailable | Deterministic behavior is unchanged | | Cloud job rejected by local policy | Rejection remains an auditable fact; cloud intent never bypasses the edge | --- # AetherIoT platform overview AetherIoT is the open-source project identity for a family of interoperable, edge-first IoT products. It is not a fourth runtime and does not own a separate wire protocol. ```text AetherIoT ├── AetherEdge edge runtime, Kernel, CLI, and SDK ├── AetherCloud cloud fusion and governed control plane └── AetherContracts public specifications, Schemas, fixtures, and TCK AetherEMS energy-management solution built on the platform ``` ## Product boundaries | Product | Owns | Does not own | | --- | --- | --- | | AetherEdge | Live point state, acquisition, deterministic rules, safety interlocks, local history, and final physical execution | Cloud placement, provider resources, or public protocol authority | | AetherCloud | Desired placement, governed cloud jobs, tenant control-plane state, and multi-cloud coordination | Edge live-state authority or provider-native actual state | | AetherContracts | Language-neutral protocol semantics, closed Schemas, fixtures, stable failure classes, and executable conformance evidence | Product runtime behavior, credentials, cloud durability, or deployment policy | | AetherEMS | Energy-domain models, workflows, and solution experience | The industry-neutral platform core | Every infrastructure provider remains authoritative for the actual existence and provider-native state of its resources. Cloud failure must not stop a commissioned AetherEdge runtime. ## Naming rules - Use **AetherIoT** for the project, community, website, and complete platform. - Use **AetherEdge** for the repository and product that were formerly named AetherIot. - Keep existing `aether-*` crate names, the `aether` CLI, the `aether-edge-sdk` package, installer names, and protocol identifiers stable. - Preserve historical release artifacts and digest-pinned AetherContracts bundles byte for byte. A new display name never rewrites old evidence. Read the [AetherIot to AetherEdge migration guide](/en/migration/aetheriot-to-aetheredge) for repository and automation changes. ## Documentation ownership This site is the common entry point. Each product repository remains the source of truth for its implementation details, while AetherContracts remains the sole authority for shared protocol behavior. The unified pages link to those sources instead of copying normative content into a second authority. Continue with [deployment topologies](/en/overview/deployment-topologies), [user journeys](/en/overview/user-journeys), or the [Edge to Contracts to Cloud tutorial](/en/tutorials/edge-contracts-cloud). --- # Typical user journeys Choose the shortest path that matches your role. All write paths remain governed and deny by default. ## Evaluate a local edge runtime 1. Open the [AetherEdge overview](/en/aetheredge). 2. Start the safe-empty composition with no devices or external services. 3. Inspect runtime health and the machine-readable manifest. 4. Add a protocol adapter and domain Pack only when the application requires it. ## Build an edge application 1. Generate clients from the running AetherEdge OpenAPI contract. 2. Start read-only and preserve quality, freshness, topology generation, and revision fields. 3. Use the authenticated application boundary; never write SHM or SQLite directly. 4. Add governed commands only with explicit permission, confirmation, idempotency, and audit behavior. ## Connect an edge fleet to cloud 1. Select a tested combination in the [compatibility matrix](/en/compatibility/version-matrix). 2. Verify the digest-pinned AetherContracts consumer lock in both products. 3. Follow the [Edge to Contracts to Cloud tutorial](/en/tutorials/edge-contracts-cloud). 4. Keep CloudLink experimental and the legacy path available until every published release gate passes. ## Implement an independent client or runtime 1. Read the [AetherContracts overview](/en/aethercontracts). 2. Implement the normative specification and closed Schemas. 3. Execute the public fixtures and black-box TCK. 4. Report conformance evidence without claiming product deployment or production authentication. ## Adopt AetherEMS Use AetherEMS when the desired outcome is an energy-management solution rather than a general-purpose edge platform. AetherEMS supplies energy semantics and workflows while the platform products keep their industry-neutral boundaries. --- # CLI Reference `aether` (version 0.5.0) is the unified management tool for Aether. It covers configuration management (`setup`, `sync`, `status`, `init`, `export`) and service operations (`channels`, `models`, `rules`, `services`, `logs`, and more). Every section below is generated from the binary's own `--help` output. ``` Usage: aether [OPTIONS] ``` Use `aether --help` for the same information at the terminal. ## Global flags These flags are accepted by every command: | Flag | Description | |------|-------------| | `-v, --verbose` | Enable verbose logging | | `--no-color` | Disable colored output | | `--json` | Output as JSON (suppresses banner and color; for scripts and AI agents) | | `--host ` | Target host for remote operations (overrides localhost default) | | `-c, --config-path ` | Configuration directory; overrides environment and installed layout | | `--db-path ` | Database directory; overrides environment and installed layout | | `-h, --help` | Print help | | `-V, --version` | Print version | With `--json`, results are written to stdout as a `{success, ...}` envelope (see [Exit codes and JSON mode](#exit-codes-and-json-mode)) and diagnostics go to stderr. The `mcp` command is the exception: it speaks MCP JSON-RPC over stdio, so `--json` does not change its output. The help output declares no environment variables; host and path defaults come from the flags above. ## aether setup Plan or apply the conservative first-run configuration. With no subcommand, `setup` is identical to `setup plan` and is persistently read-only. ``` Usage: aether setup [COMMAND] Commands: plan Recompute and print the read-only setup plan apply Apply an unchanged safe plan after explicit confirmation ``` ```bash # Human-readable, read-only plan aether setup # Structured plan for an AI agent or script aether --json setup # The only persistent setup operation aether setup apply --plan-id ``` The SHA-256 plan ID binds the target paths, safe-file fingerprints, detected extra files, and SQLite state. Apply recomputes it before writing and rejects a stale ID. Site states are: | State | Meaning | Apply behavior | |-------|---------|----------------| | `fresh` | No configuration or local database exists | Creates only the four safe empty files and local SQLite state | | `safe_partial` | An exact subset of the safe files/database exists | Preserves existing files and creates only missing safe state | | `safe_ready` | Safe empty configuration is already synchronized | Successful no-op | | `existing` | A complete custom or commissioned site was detected | Refused; zero writes | | `blocked` | A partial custom, unreadable, or unrecognized site was detected | Refused; zero writes and explicit blockers | Even a successful apply reports `ready: false`: it never starts services, enables devices or rules, performs physical control, or installs a domain pack. Continue with `aether services start` and `aether doctor`; device commissioning is a separate audited operation. ## aether runtime-manifest Verify the composition-provided runtime metadata before installing a Pack or starting services. With no `--path`, the command reads `/runtime-manifest.json` and also requires its target OS and architecture to match the current process. An explicit artifact path verifies schema, Aether version, known capabilities/features, exact feature-derived protocols, and checksum without binding a staged artifact to the verifier host. ```bash aether runtime-manifest aether --json runtime-manifest --path ./runtime-manifest.json ``` There is no full-distribution fallback: a missing, tampered, or incompatible manifest is an error even when `packs: []`. ## aether packs Build or install a Pack-only artifact. These are local filesystem operations; `--host` is ignored. ```text Usage: aether packs [OPTIONS] Commands: build Build a data-only Pack bundle bound to one Kernel runtime manifest install Verify, publish, and atomically activate a Pack bundle ``` ```bash aether packs build \ --pack-root ./packs/example \ --runtime-manifest ./runtime-manifest.json \ --output ./example.bundle aether packs install --artifact ./example.bundle ``` `build` validates `pack.yaml` against the supplied, checksummed runtime manifest and refuses Kernel/build directories, source files, executables, symlinks, and unbounded payloads. `install` requires the installed Kernel's version, target, and full runtime-manifest digest to match, publishes to `/packs//`, and atomically updates `global.yaml` only after validating the complete candidate active Pack set. It does not start services or commission devices. ## aether sync Sync all configuration to SQLite database. ``` Usage: aether sync [OPTIONS] ``` | Flag | Description | |------|-------------| | `-n, --dry-run` | Validate only, don't write to database (dry run) | | `-f, --force` | Replace sync-managed rows after successful validation; refused while any governed action route exists | | `-d, --detailed` | Show detailed progress for each item | | `--check` | Check database consistency (duplicates, references) | ```bash aether sync --dry-run ``` ## aether status Show current configuration status. ``` Usage: aether status [OPTIONS] ``` | Flag | Description | |------|-------------| | `-d, --detailed` | Show detailed status | ```bash aether status --detailed ``` ## aether init Initialize database schema (migration-only, safe upgrade). No command-specific flags. ``` Usage: aether init [OPTIONS] ``` ```bash aether init ``` ## aether export Export configuration from SQLite to YAML/CSV. ``` Usage: aether export [OPTIONS] ``` | Flag | Description | |------|-------------| | `-O, --output ` | Output directory (default: `config/`) | | `-d, --detailed` | Show detailed export progress | ```bash aether export -O /tmp/config-backup ``` ## aether channels Manage communication channels and protocols. ``` Usage: aether channels [OPTIONS] ``` Subcommands: `list`, `status`, `control`, `adjust`, `reload`, `health`, `create`, `update`, `delete`, `enable`, `disable`, `mappings`, `unmapped-points`, `write`, `points`. ### channels list List all configured communication channels. ``` Usage: aether channels list [OPTIONS] ``` ```bash aether channels list --json ``` ### channels status Get status of a specific channel. ``` Usage: aether channels status [OPTIONS] ``` ```bash aether channels status 1001 ``` ### channels reload Reconcile every channel runtime from authoritative desired state. The command name is retained for compatibility, but it calls the canonical governed `POST /api/channels/reconcile` endpoint rather than the legacy reload route. ``` Usage: aether channels reload [OPTIONS] --confirmed ``` | Flag | Description | |------|-------------| | `--confirmed` | Explicitly confirm this high-risk runtime reconciliation; requires `AETHER_ACCESS_TOKEN` | ```bash AETHER_ACCESS_TOKEN='' aether channels reload --confirmed ``` The receipt reports the sanitized desired-state observation and runtime projection for each channel, plus `degraded_count`, `reconciliation_required`, and terminal `completion_audit`. Preserve its UUID `request_id`; this operation can reconnect protocol sessions and is non-idempotent, so never retry it automatically, including after an incomplete terminal audit. ### channels health Check communication service health. ``` Usage: aether channels health [OPTIONS] ``` ```bash aether channels health --json ``` ### channels create Create a new communication channel. ``` Usage: aether channels create [OPTIONS] --name --protocol --params --confirmed ``` | Flag | Description | |------|-------------| | `--name ` | Channel name (must be unique) | | `--protocol ` | Protocol type (`modbus_tcp`, `modbus_rtu`, `virtual`, `di_do`, `can`) | | `--params ` | Protocol parameters as JSON string (e.g. `'{"host":"192.168.1.10","port":502}'`) | | `--description ` | Channel description | | `--enabled ` | Start channel immediately (default: false) [possible values: `true`, `false`] | | `--id ` | Override channel ID (auto-assigned if omitted) | | `--confirmed` | Explicitly confirm this high-risk commissioning mutation; requires `AETHER_ACCESS_TOKEN` | ```bash AETHER_ACCESS_TOKEN='' aether channels create \ --name pcs-main --protocol modbus_tcp \ --params '{"host":"192.168.1.10","port":502}' --confirmed ``` ### channels update Update an existing channel's configuration. ``` Usage: aether channels update [OPTIONS] ``` | Flag | Description | |------|-------------| | `--name ` | New channel name | | `--params ` | Updated protocol parameters as JSON string | | `--description ` | Updated description | | `--expected-revision ` | Required desired-state compare-and-set guard from the latest channel read; must be at least 1 | | `--confirmed` | Explicitly confirm this high-risk commissioning mutation; requires `AETHER_ACCESS_TOKEN` | ```bash AETHER_ACCESS_TOKEN='' aether channels update 1001 \ --description "PCS main feed" --expected-revision 7 --confirmed ``` ### channels delete Delete a channel and its measurement-owned points, mappings, and routing. The command fails closed while a physical action route targets the channel; delete or migrate that route with the governed routing command first. ``` Usage: aether channels delete [OPTIONS] ``` | Flag | Description | |------|-------------| | `-f, --force` | Skip the interactive prompt only; it never replaces `--confirmed` | | `--expected-revision ` | Required desired-state compare-and-set guard from the latest channel read; must be at least 1 | | `--confirmed` | Explicitly confirm this high-risk commissioning mutation; requires `AETHER_ACCESS_TOKEN` | ```bash AETHER_ACCESS_TOKEN='' aether channels delete 1001 \ --force --expected-revision 7 --confirmed ``` ### channels enable Enable a channel. ``` Usage: aether channels enable [OPTIONS] ``` | Flag | Description | |------|-------------| | `--expected-revision ` | Required desired-state compare-and-set guard from the latest channel read; must be at least 1 | | `--confirmed` | Explicitly confirm this high-risk lifecycle mutation; requires `AETHER_ACCESS_TOKEN` | ```bash AETHER_ACCESS_TOKEN='' aether channels enable 1001 \ --expected-revision 7 --confirmed ``` ### channels disable Disable a channel. ``` Usage: aether channels disable [OPTIONS] ``` | Flag | Description | |------|-------------| | `--expected-revision ` | Required desired-state compare-and-set guard from the latest channel read; must be at least 1 | | `--confirmed` | Explicitly confirm this high-risk lifecycle mutation; requires `AETHER_ACCESS_TOKEN` | ```bash AETHER_ACCESS_TOKEN='' aether channels disable 1001 \ --expected-revision 7 --confirmed ``` The five channel commissioning and lifecycle mutations call the governed `io.channel.manage` application boundary. Success may report a degraded runtime projection after desired state has committed. Preserve `request_id`, inspect `resulting_revision` and `reconciliation_required`, and do not automatically retry the non-idempotent command. Update, delete, enable, and disable require the revision returned by the latest channel read and fail before HTTP when it is absent. `channels reload` is the sixth governed channel command and maps separately to `io.channel.reconcile` while requiring the same `io.channel.manage` permission, explicit confirmation, Bearer token, UUID request ID, and audit policy. ### channels mappings Show a channel's point mappings. ``` Usage: aether channels mappings [OPTIONS] ``` ```bash aether channels mappings 1001 ``` ### channels unmapped-points List points on a channel with no protocol address mapping. ``` Usage: aether channels unmapped-points [OPTIONS] ``` ```bash aether channels unmapped-points 1001 ``` ### channels write Inject a simulated telemetry or signal value into the acquisition SHM plane. This command accepts only T/S points; real C/A device commands must use `aether models instances action` so routing, confirmation, and audit cannot be bypassed. ``` Usage: aether channels write [OPTIONS] --type --id --value ``` | Flag | Description | |------|-------------| | `--type ` | Simulation point type: `T` \| `S` | | `--id ` | Point ID (numeric or semantic) | | `--value ` | Value to write | ```bash aether channels write 1001 --type T --id 3 --value 42.5 ``` ### channels points list List points (grouped by T/S/C/A). ``` Usage: aether channels points list [OPTIONS] ``` | Flag | Description | |------|-------------| | `--type ` | Filter by point type: `T`, `S`, `C`, or `A` | ```bash aether channels points list 1001 --type T ``` ### channels points add Add a point to a channel. Positional arguments: `` `` (T telemetry, S signal, C control, A adjustment) ``. ``` Usage: aether channels points add [OPTIONS] --name ``` | Flag | Description | |------|-------------| | `--name ` | Signal name | | `--unit ` | Unit (e.g., V, A, kW) | | `--scale ` | Scale factor | | `--description ` | Description | | `--data-type ` | Data type (default: float32 for T/A, bool for S/C) | ```bash aether channels points add 1001 T 101 --name voltage --unit V --scale 0.1 ``` ### channels points update Update a point's attributes. ``` Usage: aether channels points update [OPTIONS] ``` | Flag | Description | |------|-------------| | `--name ` | Signal name | | `--unit ` | Unit | | `--scale ` | Scale factor | | `--description ` | Description | ```bash aether channels points update 1001 T 101 --scale 0.01 ``` ### channels points remove Remove a point from a channel. ``` Usage: aether channels points remove [OPTIONS] ``` | Flag | Description | |------|-------------| | `-f, --force` | Force deletion without confirmation | ```bash aether channels points remove 1001 T 101 --force ``` ### channels points batch Batch create/update/delete points from a JSON file. ``` Usage: aether channels points batch [OPTIONS] --file ``` | Flag | Description | |------|-------------| | `--file ` | Path to a JSON file: `{"create":[],"update":[],"delete":[]}` | ```bash aether channels points batch 1001 --file points.json ``` ### channels points mapping Show the instance mapping for a single point. ``` Usage: aether channels points mapping [OPTIONS] ``` ```bash aether channels points mapping 1001 T 101 ``` ## aether models Manage product templates and device instances. Two subcommand groups: `products` and `instances`. ``` Usage: aether models [OPTIONS] ``` ### models products list Show products selected by validated active Packs and site configuration. ``` Usage: aether models products list [OPTIONS] ``` ```bash aether models products list --json ``` ### models products available List product definitions in the `products/` directory. ``` Usage: aether models products available [OPTIONS] ``` ```bash aether models products available ``` ### models products get Show detailed information about a selected product. ``` Usage: aether models products get [OPTIONS] ``` ```bash aether models products get battery ``` ### models instances list Show all device instances. ``` Usage: aether models instances list [OPTIONS] ``` | Flag | Description | |------|-------------| | `-p, --product ` | Filter by product type | ```bash aether models instances list --product battery ``` ### models instances create Create a new device instance from a product template. Positional arguments: `` ``. ``` Usage: aether models instances create [OPTIONS] ``` | Flag | Description | |------|-------------| | `-p, --props ` | Properties in `key=value` format | ```bash aether models instances create battery bat-01 --props capacity=100 ``` ### models instances get Show detailed information about an instance. ``` Usage: aether models instances get [OPTIONS] ``` ```bash aether models instances get bat-01 ``` ### models instances update Update instance properties. ``` Usage: aether models instances update [OPTIONS] ``` | Flag | Description | |------|-------------| | `-p, --props ` | Properties to update in `key=value` format | ```bash aether models instances update bat-01 --props capacity=120 ``` ### models instances delete Delete a device instance. The command fails closed while the instance owns a physical action route; delete or migrate that route with the governed routing command first. ``` Usage: aether models instances delete [OPTIONS] ``` | Flag | Description | |------|-------------| | `-f, --force` | Force deletion without confirmation | ```bash aether models instances delete bat-01 --force ``` ### models instances data Get realtime measurement and action point data from the authoritative SHM plane. ``` Usage: aether models instances data [OPTIONS] ``` | Flag | Description | |------|-------------| | `-t, --point-type ` | Point type filter (M for measurements, A for actions, both if not specified) | ```bash aether models instances data 9 --point-type M ``` ### models instances action Submit a confirmed control action to the local command plane. A successful response does not prove that the physical device executed it; read back the corresponding measurement to verify the outcome. If the returned `audit.status` is `incomplete`, retain `request_id` and `command_id`; the action was already accepted and must not be retried. Set `AETHER_ACCESS_TOKEN` to a current Admin or Engineer access token before running this command; forged actor/role headers and local-port access do not grant device-control permission. ``` Usage: aether models instances action [OPTIONS] --point-id --value ``` | Flag | Description | |------|-------------| | `--point-id ` | Numeric action point ID encoded as a string, e.g. `"1"` | | `--value ` | Value to write | | `--confirmed` | Explicitly confirm this high-risk device command | ```bash AETHER_ACCESS_TOKEN='' \ aether models instances action 9 --point-id 1 --value 50 --confirmed ``` ## aether rules Manage and execute business rules. ``` Usage: aether rules [OPTIONS] ``` ### rules list List all configured business rules. ``` Usage: aether rules list [OPTIONS] ``` | Flag | Description | |------|-------------| | `--enabled` | Show only enabled rules | ```bash aether rules list --enabled ``` ### rules get Show detailed information about a rule. ``` Usage: aether rules get [OPTIONS] ``` ```bash aether rules get 3 ``` ### rules enable Enable a business rule. ``` Usage: aether rules enable [OPTIONS] ``` | Flag | Description | |------|-------------| | `--confirmed` | Explicitly confirm this high-risk rule-policy mutation | ```bash AETHER_ACCESS_TOKEN='' aether rules enable 3 --confirmed ``` ### rules disable Disable a business rule. ``` Usage: aether rules disable [OPTIONS] ``` | Flag | Description | |------|-------------| | `--confirmed` | Explicitly confirm this high-risk rule-policy mutation | ```bash AETHER_ACCESS_TOKEN='' aether rules disable 3 --confirmed ``` ### rules execute Execute a rule (evaluate and execute if conditions met). If the returned `audit.status` is `incomplete`, retain `request_id`; execution already completed and must not be retried. ``` Usage: aether rules execute [OPTIONS] ``` | Flag | Description | |------|-------------| | `--confirmed` | Explicitly confirm that the rule may dispatch real device commands | ```bash AETHER_ACCESS_TOKEN='' \ aether rules execute 3 --confirmed ``` ### rules create Create a new business rule. ``` Usage: aether rules create [OPTIONS] --name ``` | Flag | Description | |------|-------------| | `--name ` | Rule name | | `--description ` | Rule description | | `--confirmed` | Explicitly confirm this high-risk rule-policy mutation | ```bash AETHER_ACCESS_TOKEN='' \ aether rules create --name night-charge --description "Charge during off-peak hours" --confirmed ``` ### rules update Update rule metadata and/or flow logic. ``` Usage: aether rules update [OPTIONS] ``` | Flag | Description | |------|-------------| | `--name ` | New rule name | | `--description ` | New description | | `--enabled ` | Enable or disable the rule [possible values: `true`, `false`] | | `--priority ` | Rule priority (lower = higher priority) | | `--cooldown-ms ` | Cooldown between executions in milliseconds | | `--flow-json ` | Path to Vue Flow JSON file (use `-` for stdin) | | `--confirmed` | Explicitly confirm this high-risk rule-policy mutation | ```bash AETHER_ACCESS_TOKEN='' \ aether rules update 3 --flow-json flow.json --confirmed ``` ### rules delete Delete a business rule. ``` Usage: aether rules delete [OPTIONS] ``` | Flag | Description | |------|-------------| | `-f, --force` | Skip confirmation prompt | | `--confirmed` | Required safety confirmation; `--force` does not replace it | ```bash AETHER_ACCESS_TOKEN='' \ aether rules delete 3 --force --confirmed ``` ## aether routing Manage channel-to-instance point routing. ``` Usage: aether routing [OPTIONS] ``` ### routing list List routing configurations. ``` Usage: aether routing list [OPTIONS] ``` | Flag | Description | |------|-------------| | `-i, --instance ` | Filter by instance ID | | `--channel ` | Filter by channel ID | ```bash aether routing list --instance 9 ``` ### routing action Governed single-route commands for physical C/A destinations. Every operation requires `AETHER_ACCESS_TOKEN` and `--confirmed`; changing a route does not execute a device command. ```bash AETHER_ACCESS_TOKEN='' aether routing action upsert \ 9 1 --channel-id 1001 --channel-type c --channel-point-id 7 --confirmed AETHER_ACCESS_TOKEN='' \ aether routing action delete 9 1 --confirmed AETHER_ACCESS_TOKEN='' \ aether routing action enable 9 1 --confirmed AETHER_ACCESS_TOKEN='' \ aether routing action disable 9 1 --confirmed ``` `upsert` accepts `--disabled` to commission a route without activating it. The older `routing create --point-type a ... --confirmed` form remains a compatibility alias for enabled upsert. ### routing create Create a single routing entry for an instance. ``` Usage: aether routing create [OPTIONS] --point-type --point-id --channel-id --four-remote --channel-point-id ``` | Flag | Description | |------|-------------| | `-t, --point-type ` | Point type: `m` (measurement) or `a` (action) | | `-p, --point-id ` | Instance point ID | | `--channel-id ` | Channel ID | | `-r, --four-remote ` | Four-remote type: `t` (telemetry), `s` (signal), `c` (control), `a` (adjustment) | | `-P, --channel-point-id ` | Channel point ID | | `--confirmed` | Explicitly confirm an action route that changes a physical command target; requires `AETHER_ACCESS_TOKEN` | ```bash aether routing create 9 --point-type m --point-id 101 \ --channel-id 1001 --four-remote t --channel-point-id 101 AETHER_ACCESS_TOKEN='' aether routing create 9 \ --point-type a --point-id 1 --channel-id 1001 \ --four-remote c --channel-point-id 7 --confirmed ``` ### routing batch Batch upsert routing from JSON file or stdin. The compatibility batch accepts measurement entries only. Action entries fail closed until a governed batch application command is available; create action routes one at a time with `routing create ... --point-type a --confirmed`. ``` Usage: aether routing batch [OPTIONS] --file ``` | Flag | Description | |------|-------------| | `--file ` | Path to JSON file with routing entries (use `-` for stdin) | ```bash aether routing batch 9 --file routing.json ``` ### routing delete-instance Delete all routing for an instance. Takes the instance name, not the numeric ID. ``` Usage: aether routing delete-instance [OPTIONS] ``` | Flag | Description | |------|-------------| | `-f, --force` | Skip confirmation | | `--confirmed` | Confirm deletion of physical action routes; requires `AETHER_ACCESS_TOKEN` | ```bash AETHER_ACCESS_TOKEN='' \ aether routing delete-instance bat-01 --force --confirmed ``` ### routing delete-channel Delete all routing for a channel. ``` Usage: aether routing delete-channel [OPTIONS] ``` | Flag | Description | |------|-------------| | `-f, --force` | Skip confirmation | | `--confirmed` | Confirm deletion of physical action routes; requires `AETHER_ACCESS_TOKEN` | ```bash AETHER_ACCESS_TOKEN='' \ aether routing delete-channel 1001 --force --confirmed ``` ## aether services Start, stop, and manage Aether services. All service arguments are optional; omitting them targets all services. ``` Usage: aether services [OPTIONS] ``` ### services start Start one or more Aether services. ``` Usage: aether services start [OPTIONS] [SERVICES]... ``` ```bash aether services start aether-io aether-automation ``` ### services stop Stop one or more Aether services. ``` Usage: aether services stop [OPTIONS] [SERVICES]... ``` ```bash aether services stop ``` ### services restart Restart one or more Aether services. ``` Usage: aether services restart [OPTIONS] [SERVICES]... ``` ```bash aether services restart aether-io ``` ### services status Display status of Aether services. ``` Usage: aether services status [OPTIONS] [SERVICES]... ``` ```bash aether services status --json ``` ### services logs View logs for Aether services. ``` Usage: aether services logs [OPTIONS] ``` | Flag | Description | |------|-------------| | `-f, --follow` | Follow log output | | `-n, --tail ` | Number of lines to show from the end (default: 100) | ```bash aether services logs aether-io --follow --tail 200 ``` ### services build Build Docker images for services. ``` Usage: aether services build [OPTIONS] [SERVICES]... ``` ```bash aether services build aether-io ``` ### services pull Pull latest Docker images. ``` Usage: aether services pull [OPTIONS] ``` ```bash aether services pull ``` ### services clean Clean up Docker volumes and networks. ``` Usage: aether services clean [OPTIONS] ``` | Flag | Description | |------|-------------| | `--volumes` | Also remove volumes (long form only; `-v` is the global verbose flag) | ```bash aether services clean --volumes ``` ### services refresh Force recreate containers with latest images. ``` Usage: aether services refresh [OPTIONS] [SERVICES]... ``` | Flag | Description | |------|-------------| | `-p, --pull` | Also pull latest images before recreating | | `-s, --smart` | Use smart mode (only recreate if an image changed; stateful extensions remain explicit) | ```bash aether services refresh --pull --smart ``` ## aether logs Log level control and log file viewer. ``` Usage: aether logs [OPTIONS] ``` ### logs level Set log level for a service. Positional arguments: `` (io, automation, all) and `` (trace, debug, info, warn, error) or a full filter spec such as `"info,io=debug"`. ``` Usage: aether logs level [OPTIONS] ``` ```bash aether logs level all debug ``` ### logs get Get current log level for a service (aether-io, aether-automation, all). ``` Usage: aether logs get [OPTIONS] ``` ```bash aether logs get aether-io ``` ### logs list List log files on disk (default: today). The service filter is optional. ``` Usage: aether logs list [OPTIONS] [SERVICE] ``` | Flag | Description | |------|-------------| | `-d, --date ` | Date in `YYYYMMDD` format (default: today) | ```bash aether logs list aether-io --date 20260709 ``` ### logs view View recent lines from a service log file (aether-io, aether-automation, aether-history, aether-uplink, alarm, api). ``` Usage: aether logs view [OPTIONS] ``` | Flag | Description | |------|-------------| | `-n, --lines ` | Number of lines from end (default: 50) | | `--api` | Show API access log instead of main log | | `-g, --grep ` | Filter lines containing this pattern (case-insensitive) | ```bash aether logs view aether-io -n 100 --grep ERROR ``` ### logs tail Tail a service log file in real-time. ``` Usage: aether logs tail [OPTIONS] ``` | Flag | Description | |------|-------------| | `--api` | Show API access log instead of main log | | `-g, --grep ` | Filter lines containing this pattern (case-insensitive) | ```bash aether logs tail aether-automation --grep ERROR ``` ### logs ui Open interactive log viewer with scroll, search, and follow. ``` Usage: aether logs ui [OPTIONS] ``` | Flag | Description | |------|-------------| | `--api` | Show API access log instead of main log | ```bash aether logs ui aether-io ``` ## aether shm Zero-latency shared memory CLI (like mysql-cli). The subcommand is optional; running bare `aether shm` opens the shared-memory file directly for an interactive session (it fails if the SHM file does not exist yet). ``` Usage: aether shm [OPTIONS] [COMMAND] ``` ### shm get Get point value. Key format: `inst::M|A:` or `ch::T|S|C|A:`. ``` Usage: aether shm get [OPTIONS] ``` ```bash aether shm get inst:9:M:101 ``` ### shm info Show shared memory statistics. ``` Usage: aether shm info [OPTIONS] ``` ```bash aether shm info --json ``` ### shm watch Watch key for changes (real-time monitoring). ``` Usage: aether shm watch [OPTIONS] ``` | Flag | Description | |------|-------------| | `-i, --interval-ms ` | Polling interval in milliseconds (default: 500) | ```bash aether shm watch ch:1001:T:101 --interval-ms 200 ``` ### shm top Real-time TUI dashboard (like htop). ``` Usage: aether shm top [OPTIONS] ``` ```bash aether shm top ``` ## aether doctor Check system health and diagnose issues. For this command, `-v, --verbose` shows detailed information (response times, etc.). ``` Usage: aether doctor [OPTIONS] ``` ```bash aether doctor --verbose ``` ## aether templates Manage channel configuration templates. ``` Usage: aether templates [OPTIONS] ``` ### templates list List all channel templates. ``` Usage: aether templates list [OPTIONS] ``` | Flag | Description | |------|-------------| | `-p, --protocol ` | Filter by protocol type | ```bash aether templates list --protocol modbus_tcp ``` ### templates get Show detailed information about a template. ``` Usage: aether templates get [OPTIONS] ``` ```bash aether templates get 3 ``` ### templates snapshot Snapshot a channel's configuration as a reusable template. ``` Usage: aether templates snapshot [OPTIONS] --name ``` | Flag | Description | |------|-------------| | `-n, --name ` | Template name | | `-d, --description ` | Template description | ```bash aether templates snapshot 1001 --name pcs-modbus-template ``` ### templates apply Apply a template to a target channel. ``` Usage: aether templates apply [OPTIONS] ``` | Flag | Description | |------|-------------| | `--clear` | Clear existing points before applying | | `--slave-id ` | Override slave ID for Modbus | ```bash aether templates apply 3 1002 --clear --slave-id 2 ``` ### templates delete Delete a channel template. ``` Usage: aether templates delete [OPTIONS] ``` | Flag | Description | |------|-------------| | `-f, --force` | Force deletion without confirmation | ```bash aether templates delete 3 --force ``` ## aether alarms Manage alarm rules (create/update/delete/enable/disable); query alerts, events, and statistics. ``` Usage: aether alarms [OPTIONS] ``` ### alarms list List currently active alerts. ``` Usage: aether alarms list [OPTIONS] ``` | Flag | Description | |------|-------------| | `--channel ` | Filter by channel ID | | `--level ` | Filter by warning level (1=low, 2=medium, 3=high) | | `--keyword ` | Keyword search (rule name, channel, point) | | `--page ` | Page number, 1-based (default: 1) | | `--size ` | Page size (default: 50) | ```bash aether alarms list --level 3 ``` ### alarms get Get details of a specific active alert. ``` Usage: aether alarms get [OPTIONS] ``` ```bash aether alarms get 42 ``` ### alarms resolve Manually clear one active alert indication. If the underlying condition still holds, the monitor will create a new alert on a later evaluation. ``` Usage: aether alarms resolve [OPTIONS] --confirmed ``` ```bash AETHER_ACCESS_TOKEN='' \ aether alarms resolve 42 --confirmed ``` ### alarms rules List alarm rules. ``` Usage: aether alarms rules [OPTIONS] ``` | Flag | Description | |------|-------------| | `--channel ` | Filter by channel ID | | `--enabled` | Show only enabled rules | | `--level ` | Filter by warning level (1=low, 2=medium, 3=high) | | `--keyword ` | Keyword search | | `--page ` | Page number, 1-based (default: 1) | | `--size ` | Page size (default: 50) | ```bash aether alarms rules --enabled ``` ### alarms rule-get Get details of a specific alarm rule. ``` Usage: aether alarms rule-get [OPTIONS] ``` ```bash aether alarms rule-get 7 ``` ### alarms events List historical alert events. ``` Usage: aether alarms events [OPTIONS] ``` | Flag | Description | |------|-------------| | `--rule ` | Filter by rule ID | | `--event-type ` | Filter by event type: `trigger` or `recovery` | | `--level ` | Filter by warning level (1=low, 2=medium, 3=high) | | `--keyword ` | Keyword search | | `--page ` | Page number, 1-based (default: 1) | | `--size ` | Page size (default: 50) | ```bash aether alarms events --level 3 --event-type trigger ``` ### alarms stats Show alert count and rule statistics. ``` Usage: aether alarms stats [OPTIONS] ``` ```bash aether alarms stats --json ``` ### alarms monitor Show alarm monitor loop status. ``` Usage: aether alarms monitor [OPTIONS] ``` ```bash aether alarms monitor ``` ### alarms rule-create Create an alarm rule from a JSON file. Alarm-rule creation, update, deletion, enablement, disablement, and manual alert resolution are governed high-risk policy commands. Set `AETHER_ACCESS_TOKEN` to a current Admin or Engineer access JWT and pass `--confirmed`; query commands remain token-free on the local interface. ``` Usage: aether alarms rule-create [OPTIONS] --file --confirmed ``` | Flag | Description | |------|-------------| | `--file ` | Path to a JSON file matching alarm's `CreateRuleRequest` | | `--confirmed` | Explicitly confirm the alarm-policy mutation | ```bash AETHER_ACCESS_TOKEN='' \ aether alarms rule-create --file alarm-rule.json --confirmed ``` ### alarms rule-update Update an alarm rule from a JSON file (only present fields change). ``` Usage: aether alarms rule-update [OPTIONS] --file --confirmed ``` | Flag | Description | |------|-------------| | `--file ` | Path to a JSON file matching alarm's `UpdateRuleRequest` | | `--confirmed` | Explicitly confirm the alarm-policy mutation | ```bash AETHER_ACCESS_TOKEN='' \ aether alarms rule-update 7 --file alarm-rule-patch.json --confirmed ``` ### alarms rule-delete Delete an alarm rule. ``` Usage: aether alarms rule-delete [OPTIONS] --confirmed ``` ```bash AETHER_ACCESS_TOKEN='' \ aether alarms rule-delete 7 --confirmed ``` ### alarms rule-enable Enable an alarm rule. ``` Usage: aether alarms rule-enable [OPTIONS] --confirmed ``` ```bash AETHER_ACCESS_TOKEN='' \ aether alarms rule-enable 7 --confirmed ``` ### alarms rule-disable Disable an alarm rule. ``` Usage: aether alarms rule-disable [OPTIONS] --confirmed ``` ```bash AETHER_ACCESS_TOKEN='' \ aether alarms rule-disable 7 --confirmed ``` ## aether net Manage MQTT connection, uplink config, and TLS certificates. Two subcommand groups: `mqtt` and `cert`. ``` Usage: aether net [OPTIONS] ``` ### net mqtt status Show MQTT connection status. ``` Usage: aether net mqtt status [OPTIONS] ``` ```bash aether net mqtt status --json ``` ### net mqtt config Show the current uplink configuration. ``` Usage: aether net mqtt config [OPTIONS] ``` ```bash aether net mqtt config ``` ### net mqtt config-set Replace uplink configuration from a JSON file (full `NetConfig` object). ``` Usage: aether net mqtt config-set [OPTIONS] --file ``` | Flag | Description | |------|-------------| | `--file ` | Path to a JSON file containing the complete `NetConfig` object | ```bash aether net mqtt config-set --file netconfig.json ``` ### net mqtt reconnect Reconnect the MQTT client. ``` Usage: aether net mqtt reconnect [OPTIONS] ``` ```bash aether net mqtt reconnect ``` ### net mqtt disconnect Disconnect the MQTT client. ``` Usage: aether net mqtt disconnect [OPTIONS] ``` ```bash aether net mqtt disconnect ``` ### net cert info Show installed TLS certificate info. ``` Usage: aether net cert info [OPTIONS] ``` ```bash aether net cert info ``` ### net cert delete Delete a TLS certificate by type. ``` Usage: aether net cert delete [OPTIONS] ``` `` possible values: `ca_cert`, `client_cert`, `client_key`. ```bash aether net cert delete client_cert ``` ### net cert upload Upload a TLS certificate file (max 1 MB). Accepted extensions: `.pem` `.crt` `.key` `.cer` `.p12` `.pfx`. ``` Usage: aether net cert upload [OPTIONS] --type ``` | Flag | Description | |------|-------------| | `--type ` | Certificate role [possible values: `ca_cert`, `client_cert`, `client_key`] | ```bash aether net cert upload ca.pem --type ca_cert ``` ## aether history Query historical sensor data (latest values, time-range queries). ``` Usage: aether history [OPTIONS] ``` ### history latest Get the latest historical value for a point. Positional arguments: `` (e.g. `inst:9:M` or `io:1001:T`) and ``. ``` Usage: aether history latest [OPTIONS] ``` ```bash aether history latest inst:9:M 101 ``` ### history query Query historical data for a point. ``` Usage: aether history query [OPTIONS] ``` | Flag | Description | |------|-------------| | `--from ` | Start time (ISO 8601, e.g. `2026-05-12T00:00:00Z`, or relative like `-1h`) | | `--to ` | End time (ISO 8601, defaults to now) | | `--page ` | Page number, 1-based (default: 1) | | `--size ` | Page size, max rows per page (default: 100) | ```bash aether history query inst:9:M 101 --from 2026-05-01T00:00:00Z ``` ### history channels List channels known to history. ``` Usage: aether history channels [OPTIONS] ``` ```bash aether history channels ``` ### history metrics Show historical storage metrics (row counts, data range, etc.). ``` Usage: aether history metrics [OPTIONS] ``` ```bash aether history metrics --json ``` ### history health Check history service health. ``` Usage: aether history health [OPTIONS] ``` ```bash aether history health ``` ### history batch Batch query historical data for multiple points in one request (max 20 series). ``` Usage: aether history batch [OPTIONS] --from ``` | Flag | Description | |------|-------------| | `--series ` | Series to query, format `series_key,point_id` (repeatable, max 20) | | `--from ` | Start time (ISO 8601, e.g. `2026-05-01T00:00:00Z`) | | `--to ` | End time (ISO 8601, defaults to now) | | `--limit ` | Max data points returned per series (default 1000, max 5000) | ```bash aether history batch --series inst:9:M,101 --series inst:9:M,102 \ --from 2026-05-01T00:00:00Z --limit 500 ``` ## aether top Interactive TUI dashboard for real-time monitoring. No command-specific flags. ``` Usage: aether top [OPTIONS] ``` ```bash aether top ``` ## aether mcp Run an MCP server exposing `aether`'s capabilities as tools. The server speaks MCP JSON-RPC over stdio; the global `--json` flag does not change its output. ``` Usage: aether mcp [OPTIONS] ``` | Flag | Description | |------|-------------| | `--allow-write` | Add the 22 governed write tools to the 23 always-registered read-only tools. It is only a registration gate; each invocation still requires `confirmed: true` | ```bash aether mcp --allow-write ``` The 22 writes are channel CRUD/lifecycle (`channels_create`, `channels_update`, `channels_delete`, `channels_enable`, `channels_disable`, `channels_reconcile`); `models_instances_action`, `rules_execute`; rule CRUD and lifecycle (`rules_create`, `rules_update`, `rules_delete`, `rules_enable`, `rules_disable`); alarm-rule CRUD and lifecycle (`alarms_rule_create`, `alarms_rule_update`, `alarms_rule_delete`, `alarms_rule_enable`, `alarms_rule_disable`); manual alert resolution (`alarms_resolve`); and action-route governance (`routing_action_upsert`, `routing_action_delete`, `routing_action_set_enabled`). The write-enabled catalog therefore has 45 tools in total. The MCP bridge reads `AETHER_ACCESS_TOKEN`, sends it as an `Authorization: Bearer` credential, and generates an `X-Request-ID` for each governed HTTP request. Keep returned `request_id`/`command_id` values and do not automatically retry writes after a timeout or an incomplete audit or publication response; inspect state and audit records first. Channel mutation success may contain a degraded runtime projection; use its `request_id`, `resulting_revision`, and `reconciliation_required` rather than retrying. See [AI Assistants](/en/guides/ai-assistants) for connecting MCP clients. ## Exit codes and JSON mode Observed behavior of `aether` 0.4.0: - **Exit 0** — the operation succeeded. - **Exit 1** — the operation failed (for example, a target service is unreachable). In plain mode the error is printed as `Error: `. - **Exit 2** — command-line usage error (unknown subcommand or flag); clap prints the error and a usage hint to stderr. With `--json`, results go to stdout as a single envelope and diagnostics go to stderr: ```json { "success": true, "data": { "...": "..." } } ``` On failure the envelope carries the error message instead, and the process exits with code 1: ```json { "success": false, "error": "error sending request for url (...): tcp connect error: Connection refused" } ``` `--json` also suppresses the banner and colored output, which makes it the recommended mode for scripts and AI agents. The `mcp` command ignores it, as noted above. ## Related pages - [Getting Started](/en/guides/getting-started) — build, initialize, and start Aether - [AI Assistants](/en/guides/ai-assistants) — drive the CLI and MCP server from an AI agent - [System Architecture](/en/concepts/architecture) — the services these commands manage --- # Experimental CloudLink MQTT v1 edge contract ## Status and scope This document describes the experimental CloudLink candidate. The public AetherContracts `v0.1.0-alpha.3` release is the sole authority, and AetherEdge and AetherCloud commit the same digest-pinned consumer lock. The current claim is complete distribution integrity, full fixture execution, and an opt-in real dual-product alpha harness, not production interoperability conformance. The public AetherContracts release and compatibility guide define the shared protocol; this page only explains the AetherEdge integration. The lock imports the exact alpha.3 adoption closure. Strict Runtime Manifest SemVer and replay/digest/cursor context vectors execute in both products. Local wire, authentication, gate, scenario, and fixture-manifest files are migration or product integration material and cannot override AetherContracts. Implemented by this slice: - strict transport-neutral JSON values and canonical business digests; - session/version/epoch validation; - Runtime Manifest and real `PointSample` business mappings; - memory and crash-recoverable file spools removed only by application ACK; - MQTT v3.1.1/QoS 1 binding for a user-selected broker; - deterministic fake-transport tests, an Edge-only Broker harness, and an opt-in real Mosquitto AetherEdge/AetherCloud dual harness. Present in the imported experimental subset: - every wire field, topic, receipt, and replay/data-loss exchange; - the exact alpha.3 challenge, Gateway-signature, and trusted-connector origin proposal shapes; production key lifecycle remains unresolved; - edge-native telemetry without a mandatory Thing Model revision. The incompatible repository-local AetherEdge and AetherCloud vocabularies are superseded by public alpha.3; migration findings are retained in `contracts/cloudlink/v1/MIGRATION.md`. Planned outside this slice: - production AetherCloud authentication and durable stores; - production enrollment/CA/KMS lifecycle; - MQTT 5 enhancements and private-broker bridge/site connector; - jointly published ACL templates and production enablement; - alarms and operational telemetry on dedicated CloudLink streams; - an explicit expiry-to-data-loss lifecycle for records that expire before a cloud receipt; expired content already fails closed and is never offered. Deprecated but retained: - the unversioned `property/{productSN}/{deviceSN}` and related legacy namespace; - removal after MQTT client acceptance through the generic outbox; - legacy MQTT device-control topics. CloudLink v1 exposes no physical control or arbitrary RPC. ## Compatibility matrix | Concern | AetherEdge before this slice | AetherCloud reference | Candidate resolution | |---|---|---|---| | Wire package | Unversioned legacy JSON | Strict TypeScript codec/ingress | Byte-identical alpha.3 schemas/fixtures and matching Rust/TypeScript vocabulary | | Delivery removal | After local `AsyncClient::publish` acceptance | Durable application ACK required; memory foundations only | Dedicated spool removes only after validated durable ACK | | Session | No CloudLink session/epoch | Domain/application memory implementation | Candidate hello/accepted and monotonic epoch binding | | Authentication | MQTT username/password or mTLS; product/device topic identity | Session verifier consumes alpha structural evidence | Hello carries the exact challenge and Gateway signature object, or trusted adapter metadata is required outside payload; production key lifecycle remains proposed | | Resume | Broker reconnect only | Server cursor intended as authority | Server cursor drives stable identity/digest replay | | Telemetry identity | Legacy logical map loses timestamp/quality/address | Cloud codec now accepts the Edge fields and optional model | Preserve edge `PointAddress`, timestamp, quality and batch position without fabricating a model; Cloud multi-sample internal indexing remains open | | Topology | Coherent SHM publication epoch and snapshot digest | No equivalent batch field | Carry publication epoch and topology digest per batch | | Manifest | Closed v1, JCS SHA-256, implemented | Matching runtime-manifest domain shape | Embed the exact verified manifest and checksum | | Broker | Configurable legacy endpoint | Alpha MQTT ingress implemented | User-selected shared MQTT v3.1.1 broker is exercised by the dual harness; production authentication remains blocked | | Control | Legacy write/call topics reach governed application boundary | CloudLink commands planned separately | No CloudLink v1 command topic or payload | ## Topic policy All candidate publications and subscriptions use QoS 1 and `retain = false`. The prefix is configurable. Prefix and gateway segments reject empty values, `+`, `#`, NUL/control characters, and path traversal-like empty segments. A gateway receives only its own `down/session`, `down/ack`, and `down/replay` topics. The gateway ID is not authorization evidence. A Cloud ingress must compare it to verified publisher identity. With a generic shared broker, a separate Cloud subscriber normally cannot see the original publisher's authenticated Broker principal. Alpha.3 freezes experimental challenge/session/uplink signing objects. Production generic Broker mode still requires key provisioning, rotation, revocation, and verifier ownership. The alternate origin model is configured trusted-adapter evidence outside the payload for every publish. ## Time, integers, bounds, and digest - Protocol `uint64` values are canonical decimal strings: `0` or a non-zero digit followed by digits, with no sign, whitespace, exponent, or leading zero. - Protocol timestamps are Unix milliseconds encoded as canonical decimal strings. The embedded Runtime Manifest retains its existing field formats unchanged. - One encoded message is at most 256 KiB and one point batch contains at most 256 samples. - Identifiers and metadata are length bounded; unknown object fields fail closed. - Delivery digest is `sha256:<64 lowercase hex>` over RFC 8785 canonical JSON of the versioned business content only. Session data, trace context, retry counts, MQTT packet identifiers/properties, and transport timestamps are excluded. Equal `(gateway_id, stream_id, stream_epoch, position)` with the same stable `batch_id` and digest binding is a replay. Changing either binding at that identity is a security conflict. ## Spool and resume behavior The spool persists stream epoch, next position, records, delivery state, the last durable ACK, and data-loss evidence. Client or broker delivery cannot remove a record. A durable ACK validates all of: - current session ID and session epoch; - stream ID and stream epoch; - contiguous acknowledged position; - terminal batch identity and digest; - non-empty receipt identity. Duplicate ACKs return the prior idempotent result. Older sessions, positions past a gap, wrong batch/digest values, and conflicts fail closed. Reconnect offers the same stored content under the new session envelope; it never allocates another position, batch ID, or digest. Configured capacity is bounded to 1–65,536 retained records; each record payload is independently bounded to 256 KiB. At capacity, the adapter records the exact evicted position range. If the cloud's authoritative cursor requests an unavailable position, the edge sends that evidence and resumes at the earliest retained record only after the cloud handles the gap. A torn final journal record is truncated during recovery. Corruption in any complete journal mutation prevents opening the spool. The file adapter uses payload-once incremental mutations and atomically compacts live records and cursor metadata before accepting work after 256 mutations. ## Telemetry mapping | Edge value | Candidate field | Notes | |---|---|---| | `PointAddress::instance_id` | `instance_id` | Canonical decimal identity | | `PointAddress::kind` | `point_kind` | `telemetry`, `status`, `command`, or `action`; business collection uses acquisition-owned kinds only | | `PointAddress::point_id` | `point_id` | Canonical decimal identity | | `PointSample::value` | `value` | Must be finite | | `PointSample::timestamp` | `source_timestamp_ms` | Source Unix milliseconds | | `PointSample::quality` | `quality` | Preserved by the contract | | SHM publication epoch | `topology.publication_epoch` | Coherent point/health generation witness | | topology snapshot digest | `topology.snapshot_digest` | Identifies the exact published routing snapshot | The current SHM slot does not encode acquisition quality, so its read adapter reports accepted finite values as `good`. This is an implementation limitation, not proof that the source supplied `good` quality. No Thing Model revision is fabricated. The optional `model` binding is accepted only when it originates from commissioned, verified configuration. Cloud enrichment can map edge point addresses after ingestion. ## Runtime Manifest mapping The report embeds the exact result of the current Runtime Manifest generator. Its `checksum.algorithm` remains `sha256`; its digest is computed over RFC 8785 canonical JSON of every manifest field except `checksum`. CloudLink validates and transports that checksum rather than inventing another composition model. ## Shared-broker harnesses The Edge-only integration is disabled unless explicitly enabled. It requires an MQTT v3.1.1 broker and deliberately uses a fake Cloud peer, so it is not joint interop evidence: ```bash AETHER_CLOUDLINK_RUN_INTEGRATION=1 \ AETHER_CLOUDLINK_BROKER_HOST=127.0.0.1 \ AETHER_CLOUDLINK_BROKER_PORT=1883 \ cargo test -p aether-cloudlink-mqtt --test shared_broker -- --nocapture ``` Optional credentials use `AETHER_CLOUDLINK_BROKER_USERNAME` and `AETHER_CLOUDLINK_BROKER_PASSWORD`. TLS uses `AETHER_CLOUDLINK_BROKER_CA`, with optional client certificate/key variables. Tests and errors never print credential values. The final alpha evidence uses real Mosquitto, this repository's rumqttc transport and `FileCloudLinkSpool`, plus AetherCloud's real MQTT adapter and application use cases. Run it from AetherCloud: ```bash pnpm test:cloudlink-dual ``` It writes `AetherCloud/evidence/cloudlink-alpha3-dual-harness.json` and a compatibility copy under `artifacts/cloudlink-alpha/evidence.json`, including the fault matrix. The harness implements Broker reconnect, ACK loss, a second Edge process recovering the file spool, the Cloud-owned `manifest/1/1` resume cursor, a second Cloud ingress process, duplicate/idempotent replay, conflict, expiry, out-of-order, a non-durable partial outcome, and explicit data loss. Telemetry still replays after its lost ACK. PostgreSQL process-crash durability remains blocked, and the Cloud restart result is honestly `unknown-reaccepted`. --- # Configuration Reference Operational configuration lives in YAML, CSV, and JSON files under a `config/` directory and is imported into SQLite (`aether.db`) by `aether sync`. The one startup-time exception is `global.yaml`'s `packs` list: automation and `aether mcp` read that same entry directly so a Pack identity and root cannot drift between the two processes. ## The sync pipeline ``` config/*.yaml, *.csv, *.json → aether sync → SQLite (aether.db) → services (at startup) ``` Editing a YAML file does nothing by itself. Offline `aether sync` requires the configuration-owning services to be stopped, writes all desired-state heads in one transaction, and takes effect on the next supervised service start. Online channel, instance, routing, and rule mutations instead enter their governed application commands and reconcile their runtime projections automatically. `aether sync` (implemented in `tools/aether/src/core/syncer.rs`) processes three targets inside one site-level SQLite transaction, so a failure in any target leaves the database untouched: - **global** — parses `config/global.yaml` into the `service_config` table. - **aether-io** — parses `config/io/io.yaml` into the `channels` table and the per-channel CSV files into the four point tables (`telemetry_points`, `signal_points`, `control_points`, `adjustment_points`). Duplicate channel names abort the sync. - **aether-automation** — parses `config/automation/automation.yaml`, `instances.yaml`, and `rules/*.json` into the instance and rules tables, imports measurement (`M`) entries from `instance_routing.csv`, and validates any external product JSON files under `config/automation/products/`. There is no standalone calculation-engine sync path — a previously-orphaned `calculations.yaml` template, its unused table, and its dead API schema types have been removed. Derived quantities are expressed with `calculation` nodes inside individual rules instead (see [Control Strategies as Rules](https://github.com/EvanL1/AetherEdge/blob/main/docs/domain/control-strategies.md)). Before writing, `aether sync` validates all three domains. It then applies global, IO, and automation configuration in one SQLite transaction, so an error in a later domain rolls back all earlier changes. By default rows with no corresponding config file (for example rules created through the HTTP API) are preserved. With `--force`, managed tables are fully replaced, but validation is still mandatory. Action (`A`) routing is deliberately outside this compatibility importer: it selects the physical target of future device commands and must use the authenticated, confirmed, audited action-routing application command. An `A` row rolls back the whole sync. `--force` also refuses to start while any action route exists, so it cannot cascade-delete a commissioned command target. Delete or migrate those routes through the governed routing API before removing their instance, channel, control point, or adjustment point. Measurement routing remains sync-managed. Two related commands are easy to confuse with sync: - `aether init` initializes or upgrades the **database schema** only (`CREATE TABLE IF NOT EXISTS`, migration-only — it refuses to reset an existing database). It does not create or copy any config files. - The `config/` directory itself is scaffolded at **deploy time**: the Docker installer (`scripts/install.sh`) stages `config.template/` alongside the binaries and activates it at `/config/` only on a clean host. Any existing site configuration makes the fresh-only installer fail before it writes. Containers mount the new directory at `/app/config/`; the installer does not merge, upgrade, or import operator-owned configuration. In a development checkout, `aether setup` plans and activates only the four site-authored safe files under `./data/config` and initializes `./data/aether.db` after the returned plan ID is explicitly applied. The developer must then provide the explicit composition manifest described below; setup never guesses which IO features were compiled. ## Directory layout The repository's `config.template/` directory is the canonical fail-safe starting point. It contains no commissioned channel, device instance, or enabled control rule. Domain examples are opt-in; the energy examples live under `packs/energy/examples/config/`. Annotated: ``` config.template/ ├── global.yaml # Shared settings: active Packs, API bind │ # host, log level/rotation, rule scheduler │ # tick interval (rules.tick_ms, default 100) ├── runtime-manifest.json # Generated, checksummed build composition; │ # never inferred or edited by site setup ├── io/ │ ├── io.yaml # Empty channel list until commissioning │ │ # (modbus_tcp, modbus_rtu, can, mqtt, http, │ │ # di_do, ...), enabled flag, per-protocol │ │ # connection parameters, per-channel logging │ └── / # (expected by the syncer; not shipped in │ │ # the template) One directory per channel, │ │ # named by its numeric channel id (e.g. 1/) │ ├── telemetry.csv # T (telemetry) point definitions │ ├── signal.csv # S (signal) point definitions │ ├── control.csv # C (control) point definitions │ ├── adjustment.csv # A (adjustment) point definitions │ └── mapping/ # Protocol register mappings, one CSV per │ # point type (telemetry_mapping.csv, ...) └── automation/ ├── automation.yaml # Instance auto-load is disabled by default ├── instances.yaml # Empty instance map until commissioning ├── instances/ # Optional per-instance directories, each │ └── /instance.yaml # holding one instance definition ├── rules/ # One JSON file per control rule (Vue Flow │ └── *.json # graph: nodes, edges, priority, enabled) └── products/ # (optional, not in the template) Site-owned # product JSON files; when present they may # override models from an active Pack ``` Point-type shorthand: Aether uses T (telemetry), S (signal), C (control), and A (adjustment) for the four point classes throughout its APIs and file formats. The fail-safe default in `global.yaml` is `packs: []`, so a fresh site exposes zero domain products and no Pack-owned MCP knowledge. An installed Pack is activated with one identity-bound root: ```yaml packs: - id: energy root: /opt/aether/packs/energy ``` The manifest identity must match `id`; compatibility, capability, protocol, commissioning, and asset confinement checks must all pass. A relative `root` is resolved from the configuration directory and cannot contain `..`. If `automation.yaml` sets `products_path`, that site-owned directory is loaded last and may deliberately override a model from an active Pack. Both runtime loading and `aether sync` reject symlinks, non-regular/oversized JSON, invalid JSON, and duplicate product names within one directory. `runtime-manifest.json` is mandatory beside `global.yaml`. It is generated by the runtime composition or installer, not authored by a Pack or inferred by an individual service. The closed v1 document records the Aether release, target, included services, exact `aether-io` protocol features, derived adapters, and application capabilities under a canonical SHA-256 checksum. Automation and MCP reject missing, tampered, version-mismatched, target-mismatched, unknown, feature-inconsistent, symlinked, non-regular, or oversized manifests before activating any Pack. For an explicit local development composition, generate it with: ```bash HOST_TARGET=$(rustc -vV | sed -n 's/^host: //p') cargo run -p aether-runtime-catalog --bin aether-runtime-manifest -- \ generate "$HOST_TARGET" data/config ``` Pass a third comma-separated argument to `generate` for a deliberately trimmed IO feature set; there is no fallback that assumes all adapters are present. Use `aether runtime-manifest` (or `--path `) to run the same verifier used by the installers, Automation, and MCP. ## Environment variables Key variables used by Docker Compose and the services (most optional values are illustrated in `.env.example`; deployment overrides add required production gates): | Variable | Default | Purpose | |----------|---------|---------| | `AETHER_BASE_PATH` | `./data` | Base path for site configuration and databases; logs use `AETHER_LOG_PATH` | | `HOST_UID` | `1000` | User id for container processes; must match the host user to avoid file-permission issues | | `HOST_GID` | `1000` | Group id for container processes; pairs with `HOST_UID` | | `DIALOUT_GID` | `20` | Dialout group id for serial-port access (Linux only) | | `INFLUXDB_URL`, `INFLUXDB_ORG`, `INFLUXDB_BUCKET`, `INFLUXDB_TOKEN`, `INFLUXDB_PASSWORD` | unset | Optional InfluxDB history adapter only; unused by the default runtime | | `AETHER_IO_URL` | `http://127.0.0.1:6001` | io base URL for the API gateway and the `aether` CLI | | `AETHER_AUTOMATION_URL` | `http://127.0.0.1:6002` | automation base URL for the API gateway and the `aether` CLI | | `AETHER_SHM_PATH` | platform-selected tmpfs path | Canonical authoritative point-state segment shared by io and read-only consumers | | `AETHER_CHANNEL_HEALTH_SHM_PATH` | sibling `*-health` path | Separate authoritative channel-connectivity segment; normally derived from `AETHER_SHM_PATH` | | `SHM_WRITER_STALE_AFTER_MS` | `30000` | Maximum writer-heartbeat age accepted by read-side SHM adapters | | `SHM_IDENTITY_CHECK_INTERVAL_MS` | `250` | Fallback interval for checking whether the canonical SHM inode was replaced; generation fencing handles normal swaps immediately | | `SHM_TOPOLOGY_REFRESH_INTERVAL_MS` | `1000` (minimum `100`) | Interval used by API, alarm, and automation to reload one SQLite topology snapshot and atomically publish a validated point/health/routing generation | | `JWT_SECRET_KEY` | unset (required) | Shared 32-byte-or-longer access-JWT signing/verification secret for aether-api plus governed io, automation, and alarm operations; installers generate it and keep it outside configuration assets | | `AETHER_ACCESS_TOKEN` | unset | Signed Admin/Engineer access JWT required by governed CLI channel commissioning/lifecycle, device commands, action-routing changes, and automation/alarm policy operations, including MCP's 22 governed write tools; query commands do not require it on the local interface | | `AETHER_UPLINK_CONTROL_TOKEN` | unset | Separate 32-byte-or-longer service credential used only for uplink-to-automation device commands; installers generate it and never print it | | `AETHER_ALLOW_SIMULATION_WRITES` | `false` | Development-only opt-in for io T/S simulation writes into authoritative SHM; keep disabled in production | | `AETHER_CONFIG_PATH` | unset | Shared configuration directory used by automation and `aether mcp`; CLI path resolution may set it through deployment context or `--config-path` | | `AETHER_DATA_PATH` | unset | Overrides the install-context data directory for the `aether` CLI | | `AETHER_INSTALL_CONTEXT_PATH` | `/etc/aether/install.yaml` | Overrides the installed layout descriptor; CLI flags and the two path variables take precedence | | `AETHER_BOOTSTRAP_ADMIN_PASSWORD` | unset | Required only while `users` is empty; installers generate a strong value in their mode-0600 environment file, and it should be removed after the first password change | | `AETHER_ALLOW_PUBLIC_REGISTRATION` | `false` | Explicit opt-in for anonymous Viewer registration; Admin creation is never available through public registration | | `AETHER_DATA_PROCESSING_ENABLED` | `false` | Explicitly enables the opt-in Data Processing application and HTTP routes; startup fails closed if enabled configuration is invalid | | `AETHER_DATA_PROCESSING_CONFIG` | `/app/data/config/data-processing/runtime.yaml` | Strict runtime YAML containing commissioned task, binding, history, covariate, processor, and audit composition | | `AETHER_LOAD_FORECASTING_BEARER_TOKEN` | unset | Shared deployment secret used by `aether-api` to authenticate to the Load-Forecasting sidecar; required by the production override | | `AETHER_LOAD_FORECASTING_REQUIRE_AUTH` | `false` in development | Processor-side startup gate; the production override fixes it to `true` | | `AETHER_LOAD_FORECASTING_MAX_CONCURRENCY` | `1` | Bounds occupied model execution slots; cancellation does not release a slot until background work actually finishes | | `AETHER_LOAD_FORECASTING_ARTIFACT_BUNDLES` | unset | Strict JSON array pinning every actual commissioned model/scaler/config artifact; required for production readiness | | `AETHER_LOAD_FORECASTING_IMAGE` | mutable local development image | Production must use an immutable `@sha256` image reference through the explicit Compose override and preflight validator | | `AETHER_LOAD_FORECASTING_PORT` | `8989` | Host-loopback published processor port for the Compose sidecar | | `RUST_LOG` | `info` | Log level for the Rust services; supports filter syntax such as `info,io=debug,automation=trace` | ### Experimental CloudLink MQTT settings The current `aether-uplink` production composition stays in deprecated `legacy` mode. The experimental `aether-cloudlink-mqtt` embedding API exposes the explicit `legacy`, `cloudlink-v1`, and `dual` migration values; it does not silently enable CloudLink in an existing installation. The first real-broker vertical slice is the opt-in test harness below. These variables are read only when `AETHER_CLOUDLINK_RUN_INTEGRATION=1`: | Variable | Default | Purpose | |---|---|---| | `AETHER_CLOUDLINK_RUN_INTEGRATION` | unset | Set exactly `1` to run the external-broker harness | | `AETHER_CLOUDLINK_BROKER_HOST` | `127.0.0.1` | User-selected MQTT broker hostname/IP | | `AETHER_CLOUDLINK_BROKER_PORT` | `1883` | User-selected broker port | | `AETHER_CLOUDLINK_BROKER_USERNAME` | unset | Optional broker username | | `AETHER_CLOUDLINK_BROKER_PASSWORD` | unset | Optional write-only broker password; never printed or serialized | | `AETHER_CLOUDLINK_BROKER_TLS` | unset | Set `1` to use platform TLS roots | | `AETHER_CLOUDLINK_BROKER_CA` | unset | Custom PEM CA path; selects custom TLS when present | | `AETHER_CLOUDLINK_BROKER_CLIENT_CERT` | unset | Optional mTLS client certificate, configured with the key | | `AETHER_CLOUDLINK_BROKER_CLIENT_KEY` | unset | Optional mTLS PKCS#8 private key, configured with the certificate | | `AETHERCLOUD_ROOT` | unset | Optional read-only path used by joint orchestration outside this edge-only harness; the test does not modify or start it | Plaintext is accepted only by the explicit development harness. Production validation requires TLS. MQTT v3.1.1, QoS 1, non-retained messages, and exact per-gateway topics are fixed by the experimental CloudLink profile; MQTT 5 remains optional and cannot be required for correctness. For MCP writes, `--allow-write` only registers the 22-tool write allowlist. The bridge sends `AETHER_ACCESS_TOKEN` as an `Authorization: Bearer` credential and adds an `X-Request-ID`; every invocation still requires `confirmed: true`. Preserve returned request/command IDs and do not automatically retry a timeout or an incomplete audit/publication result. Channel mutations also return a desired-state revision and may succeed with a degraded runtime projection; inspect `request_id`, `resulting_revision`, and `reconciliation_required` instead of retrying automatically. ### Data Processing and historian storage changes The Data Processing runtime's `history.path` must name the SQLite file that the running historian actually writes. Values under `history_config.storage_*` are persisted desired settings. In particular, `PUT /hisApi/storage` saves them but does not reconnect the active backend, so matching those rows is not sufficient proof of the live writer. Change storage only with Data Processing disabled; reconnect or restart `aether-history`, verify its active backend/health and a commissioned sentinel series, then restart `aether-api` with the matching runtime path. The API also needs independent read-only OS permission to the historian database/WAL/SHM directory. Keep that path separate from the API's writable configuration/audit database. SQLite `mode=ro` over the base Compose `/app/data:rw` mount is not a completed production permission boundary. ## Related pages - [Getting Started](/en/guides/getting-started) — first setup and startup walkthrough - [Connect Devices](/en/guides/connect-devices) — channel and point configuration in practice - [Writing Rules](/en/guides/writing-rules) — the rule JSON that lives under `automation/rules/` - [HTTP API](/en/reference/http-api) — the runtime API the synced configuration feeds - [System Architecture](/en/concepts/architecture) — where each service fits --- # Data Processing Contracts This reference specifies the implemented version 1 contracts for **Aether Data Processing**. Rust domain values and orchestration live in `aether-domain`, `aether-ports`, and `aether-application`; `aether-data-processing` provides the strict transport-neutral JSON codec, and `extensions/http-data-processor` implements the optional HTTP transport. The contract encodes one architectural rule: Aether assembles a complete input frame, then requests a processor. A `DataProcessor` never receives credentials or callbacks with which to read Aether's SHM, history database, or site configuration. ## Contract family | Contract | Identifier | Purpose | |----------|------------|---------| | Task declaration | `aether.data-processing-task.v1` | Portable domain semantics and input bindings | | Application request | `aether.data-processing.process-task-request.v1` | Select a commissioned task, binding, data cut, and typed options | | Processing frame | `aether.processing-frame.v1` | Aligned observations and known-future covariates | | Processor request | `aether.data-processing.request.v1` | Resolved task and binding, deadline, complete frame, digest, and typed options | | Result envelope | `aether.data-processing.result.v1` | Status, provenance, expiry, and typed derived data | | Forecast output | `aether.data-processing.output.forecast.v1` | Processor-produced time-indexed forecast values | | Accepted derived data | `aether.derived-data.v1` | Aether-stamped, validated task output | | Error envelope | `aether.data-processing.error.v1` | Typed transport or processor failure | The HTTP adapter uses the media type: ```text application/vnd.aether.data-processing+json;version=1 ``` Contract identifiers are exact, case-sensitive strings. Version 1 consumers must reject an unsupported major version rather than guessing how to interpret it. Additive fields may be introduced only where the schema explicitly permits them; implementations must not use unknown fields to smuggle vendor-specific commands through the common envelope. The keywords **MUST**, **MUST NOT**, **SHOULD**, and **MAY** below are normative. ## Common value rules - Times MUST be RFC 3339 UTC strings ending in `Z`, at or after the Unix epoch, with no finer than millisecond precision. They map losslessly to `TimestampMs`; encoders may omit a zero fractional part, but MUST NOT emit more than three fractional digits. Processors MUST NOT silently reinterpret a local time or a numeric epoch value. - Durations and cadences MUST be integer seconds greater than zero. - Numeric values MUST be finite JSON numbers. `NaN`, positive infinity, and negative infinity are invalid. - Missing samples MUST be JSON `null` and have sample quality `missing`. - Units MUST be explicit for numeric features and outputs. A task declaration chooses the canonical unit. The current v1 runtime verifies that commissioned source metadata already matches that unit and sign convention; it does not convert either at request time. - Feature names MUST match the task declaration exactly and be unique within a segment. - Timestamps MUST be strictly increasing after Aether has resolved duplicates. - Digests use lowercase hexadecimal SHA-256 with the prefix `sha256:`. ## `ProcessingFrame` A `ProcessingFrame` carries all data the selected processor is permitted to use. It has historical observations, optional known-future covariates, optional static features, aggregate quality, and redaction-safe provenance. ### Shape ```json { "schema": "aether.processing-frame.v1", "as_of": "2026-07-11T12:00:00Z", "cadence_seconds": 900, "history": { "timestamps": ["2026-07-11T11:45:00Z", "2026-07-11T12:00:00Z"], "features": { "load": { "value_type": "number", "unit": "kW", "values": [820.0, 835.0], "quality": ["good", "good"] } } }, "future_covariates": { "timestamps": ["2026-07-11T12:15:00Z", "2026-07-11T12:30:00Z"], "features": { "temp_avg": { "value_type": "number", "unit": "Cel", "values": [32.1, 32.0], "quality": ["good", "good"] }, "quarter_hour": { "value_type": "number", "unit": "1", "values": [49, 50], "quality": ["good", "good"] } } }, "static_features": { "rated_power": { "value_type": "number", "unit": "kW", "value": 2500.0, "quality": "good" } }, "quality": { "input_watermark": "2026-07-11T11:59:58Z", "missing_ratio": 0.0, "max_gap_seconds": 900, "live_tail_included": false, "substituted_samples": 0 }, "provenance": [ { "segment": "history", "feature": "load", "source_kind": "history", "source_ref": "energy.site.load.active_power", "watermark": "2026-07-11T11:59:58Z" }, { "segment": "future_covariates", "feature": "temp_avg", "source_kind": "covariate", "source_ref": "weather.nwp.air_temperature", "watermark": "2026-07-11T11:50:00Z", "issued_at": "2026-07-11T11:40:00Z" }, { "segment": "future_covariates", "feature": "quarter_hour", "source_kind": "calendar", "source_ref": "calendar.utc.quarter_hour", "watermark": "2026-07-11T12:00:00Z" }, { "segment": "static_features", "feature": "rated_power", "source_kind": "constant", "source_ref": "energy.site.rated_power", "watermark": "2026-07-11T12:00:00Z" } ] } ``` ### Top-level fields | Field | Required | Validation | |-------|----------|------------| | `schema` | yes | Exactly `aether.processing-frame.v1` | | `as_of` | yes | Logical cutoff for the request; UTC RFC 3339 | | `cadence_seconds` | yes | Positive integer matching the task revision | | `history` | yes | At least one timestamp and one declared feature | | `future_covariates` | task-dependent | Required for tasks that declare future-known inputs; otherwise omitted | | `static_features` | no | Only features declared by the task; empty object is allowed | | `quality` | yes | Aggregate quality calculated by Aether | | `provenance` | yes | Exactly one entry for every populated `(segment, feature)` pair; only `source_ref` may be omitted by egress policy | The generic contract includes `static_features`, but the current opt-in `aether-api` runtime YAML loader cannot bind static values. They are available only to a custom composition using `DataProcessingBinding` until that loader is extended and tested. The shipped load/PV runtime route declares none. ### Segment schema Both `history` and `future_covariates` use the same structural schema: ```json { "timestamps": ["2026-07-11T12:15:00Z"], "features": { "feature_name": { "value_type": "number", "unit": "kW", "values": [123.4], "quality": ["good"] } } } ``` `value_type` is one of `number`, `string`, or `boolean`. Every series in a segment MUST have exactly as many values and quality entries as the segment has timestamps. Sample quality is one of: | Quality | Meaning | |---------|---------| | `good` | Value in the declared task representation accepted by the source adapter and task policy | | `uncertain` | Source supplied a value but marked its confidence as reduced | | `substituted` | Aether filled the sample using the task's declared method | | `missing` | No usable value; the corresponding value is `null` | For numeric series, `unit` is required. For string and boolean series, `unit` MUST be omitted. `null` is permitted only when the task's missing policy allows it. A processor MUST NOT convert a missing value to zero unless the task explicitly declares that exact substitution before the frame is built. ### Time invariants - Version 1 uses interval-end labels. For cadence `c`, a historical label `t` represents the raw interval `(t-c, t]`. - A history grid with `N` steps MUST be `as_of-(N-1)c, ..., as_of`; its final label is exactly `as_of`, and source reads MUST NOT advance beyond that cutoff. - A future-covariate grid MUST begin at `as_of+c` and advance by exact cadence. - Version 1 frames use an exact `cadence_seconds` grid. A source gap is retained as an explicit missing sample or rejected by task policy; timestamps are not silently removed or retimed. - Future target values MUST NOT appear in `future_covariates`. A feature is allowed there only when the task marks it as known ahead of time. - `input_watermark` MUST be less than or equal to `as_of` and represent the newest source observation considered, not the request creation time. ### Aggregate quality | Field | Required | Meaning | |-------|----------|---------| | `input_watermark` | yes | Newest actual observation considered by assembly | | `missing_ratio` | yes | Missing cells divided by all required cells, in `[0, 1]` | | `max_gap_seconds` | yes | Largest gap among required historical observations | | `live_tail_included` | yes | Whether read-only live state contributed after stored history | | `substituted_samples` | yes | Count of values labeled `substituted` | The aggregate does not replace per-sample quality. A processor validates both against the selected task and optional processor artifact manifest. The v1 wire contract can carry per-sample quality, but the current production sources do not preserve device-origin quality end to end. The embedded history table stores numeric observations without device quality, and the current SHM bridge labels accepted finite live values as `good`. Current commissioning can therefore enforce freshness, gaps, missingness, numeric constraints, provenance, and issue time, but deployments that require original device quality MUST add a quality-bearing source adapter. Live tail is permitted only for a history feature whose commissioned aggregation is `Last`, where one instantaneous SHM value can validly replace the final cell. Version 1 rejects live tail for `Mean`, `Sum`, `Min`, and `Max`. The current energy load and PV target histories use `Mean`, so their frames set `live_tail_included: false`. ### Provenance `segment` is `history`, `future_covariates`, or `static_features` and identifies which occurrence of a feature the entry describes. `source_kind` is one of `history`, `live`, `history_and_live`, `covariate`, `calendar`, or `constant`. `source_ref` is a semantic identifier, never a SQL query, database credential, SHM path, channel address, or model filesystem path. When a remote data-egress policy removes `source_ref`, it MUST retain the segment, feature name, source kind, and watermark. `issued_at` is optional and records when a versioned external forecast, such as an NWP run, was issued. When present, it MUST satisfy `issued_at <= watermark <= frame.as_of`. Valid times in `future_covariates.timestamps` may be later than `as_of`; the issue cut may not. This prevents assembly from selecting a covariate forecast unavailable at the requested event-time cut. It does not make the current history store or artifact selector point-in-time safe for backtesting. Every populated `(segment, feature)` pair MUST have exactly one provenance entry. Missing entries, duplicate keys with different metadata, and entries for absent features are invalid. Deterministically generated calendar values and commissioned constants are not exceptions: they use `calendar` and `constant` provenance respectively. Their watermark records the data cut used to derive the value, but they do not advance aggregate `input_watermark`, which means the newest actual observation considered by assembly. ### Implemented history adapters The production `aether-api` composition uses `SqliteHistoryQuery` by default. It opens the existing `aether-history.db` schema lazily and read-only, never creates or migrates it, and performs all feature reads for one logical request inside one SQLite read transaction. A task-scoped route fixes each feature's physical series, unit, cadence, aggregation, and duplicate policy. Raw numeric rows are reduced into interval-end buckets; empty buckets remain explicit missing values, and the watermark is the newest numeric raw observation that actually participated rather than the bucket label. `aether-history` remains the schema, write, retention, and file-lifecycle owner. The query adapter relies on SQLite snapshot/WAL behavior and deployment file permissions; a missing or inaccessible file makes only the processing request typed-unavailable and can recover on a later call. Selecting an external historian does not silently redirect this adapter—the current runtime must be recomposed with a conforming `HistoryQuery` implementation. `mode=ro` and `query_only=ON` constrain SQLite operations but do not replace OS isolation. Production MUST give the API principal only read permission to a dedicated historian directory containing the database/WAL/SHM family, while keeping its own configuration/audit database separately writable. The base Compose currently mounts `/app/data` read-write into `aether-api`; it is a development baseline, not a completed production permission boundary for direct historian reads. The transaction snapshot is read-consistent when the invocation runs, but the schema is not bitemporal. Rows have event `time_ms` only: no `ingested_at` and no source, binding, or configuration epoch. Therefore `as_of` is not a point-in-time database reconstruction. Late backfills with old event times can change a later replay, and remapping a device behind the same logical `(series_key, point_id)` can splice source epochs. Expected task/binding revisions validate current commissioning but cannot filter metadata absent from stored rows. Leakage-safe offline evaluation MUST use a frozen historian snapshot/export captured at the evaluation cut or another adapter with ingestion-time and source-epoch filtering. The composition currently validates its path against persisted `history_config.storage_*`, but those values are saved intent rather than an attestation of the active writer. `PUT /hisApi/storage` does not reconnect the historian. Across storage changes, disable Data Processing, reconnect or restart `aether-history`, verify the active SQLite backend and a commissioned sentinel series, then restart `aether-api` with the matching path. `HttpHistoryQuery` is an optional loopback adapter for an upstream service that already materializes the exact cadence grid. It accepts only `aggregation=last` and `duplicate_policy=reject`; it is not a substitute for raw SQLite aggregation. ## `ProcessTaskRequest` Application callers select a commissioned task and an event-time data cut. They do not submit a frame, endpoint, processor ID, artifact selector, credentials, or source query. ```json { "task_id": "energy.site-load-forecast", "expected_task_revision": 1, "binding_id": "site-a", "expected_binding_revision": 7, "as_of": "2026-07-11T12:00:00Z", "options": { "kind": "forecast", "horizon_steps": 2 } } ``` The transport supplies request identity and actor data through the common `RequestContext`; they are not duplicated in this typed request. `expected_task_revision` and `expected_binding_revision` make an invocation fail closed when configuration changed. `binding_id` names an enabled, commissioned binding; the application resolves its points, covariates, processor route, artifact policy, and egress policy atomically. ## `DataProcessingRequest` This is the processor-facing request assembled by Aether. It is never the public application input. ### Schema ```json { "schema": "aether.data-processing.request.v1", "request_id": "0190aee6-2139-7a87-8448-806f1b843201", "submitted_at": "2026-07-11T12:00:01Z", "deadline": "2026-07-11T12:00:06Z", "task": { "id": "energy.site-load-forecast", "revision": 1, "kind": "forecast" }, "binding": { "id": "site-a", "revision": 7 }, "processor_contract": "aether.data-processing.forecast.v1", "artifact": { "kind": "model", "family": "site-load", "version": "v3", "artifact_digest": "sha256:98967bdedc60b8ab555e596516eb272063c139ccf3a3112fb29a46ab0610f270" }, "frame": { "schema": "aether.processing-frame.v1", "as_of": "2026-07-11T12:00:00Z", "cadence_seconds": 900, "history": { "timestamps": ["2026-07-11T11:45:00Z", "2026-07-11T12:00:00Z"], "features": { "load": { "value_type": "number", "unit": "kW", "values": [820.0, 835.0], "quality": ["good", "good"] }, "temp_avg": { "value_type": "number", "unit": "Cel", "values": [31.0, 31.2], "quality": ["good", "good"] }, "humidity": { "value_type": "number", "unit": "%", "values": [64.0, 63.0], "quality": ["good", "good"] }, "rain": { "value_type": "number", "unit": "mm", "values": [0.0, 0.0], "quality": ["good", "good"] }, "quarter_hour": { "value_type": "number", "unit": "1", "values": [47, 48], "quality": ["good", "good"] } } }, "future_covariates": { "timestamps": ["2026-07-11T12:15:00Z", "2026-07-11T12:30:00Z"], "features": { "temp_avg": { "value_type": "number", "unit": "Cel", "values": [32.1, 32.0], "quality": ["good", "good"] }, "humidity": { "value_type": "number", "unit": "%", "values": [61.0, 62.0], "quality": ["good", "good"] }, "rain": { "value_type": "number", "unit": "mm", "values": [0.0, 0.0], "quality": ["good", "good"] }, "quarter_hour": { "value_type": "number", "unit": "1", "values": [49, 50], "quality": ["good", "good"] } } }, "static_features": {}, "quality": { "input_watermark": "2026-07-11T12:00:00Z", "missing_ratio": 0.0, "max_gap_seconds": 900, "live_tail_included": false, "substituted_samples": 0 }, "provenance": [ { "segment": "history", "feature": "load", "source_kind": "history", "source_ref": "energy.site.load.active_power", "watermark": "2026-07-11T12:00:00Z" }, { "segment": "history", "feature": "temp_avg", "source_kind": "history", "source_ref": "weather.observed.air_temperature", "watermark": "2026-07-11T12:00:00Z" }, { "segment": "history", "feature": "humidity", "source_kind": "history", "source_ref": "weather.observed.relative_humidity", "watermark": "2026-07-11T12:00:00Z" }, { "segment": "history", "feature": "rain", "source_kind": "history", "source_ref": "weather.observed.precipitation", "watermark": "2026-07-11T12:00:00Z" }, { "segment": "history", "feature": "quarter_hour", "source_kind": "calendar", "source_ref": "calendar.utc.quarter_hour", "watermark": "2026-07-11T12:00:00Z" }, { "segment": "future_covariates", "feature": "temp_avg", "source_kind": "covariate", "source_ref": "weather.nwp.air_temperature", "watermark": "2026-07-11T11:50:00Z", "issued_at": "2026-07-11T11:40:00Z" }, { "segment": "future_covariates", "feature": "humidity", "source_kind": "covariate", "source_ref": "weather.nwp.relative_humidity", "watermark": "2026-07-11T11:50:00Z", "issued_at": "2026-07-11T11:40:00Z" }, { "segment": "future_covariates", "feature": "rain", "source_kind": "covariate", "source_ref": "weather.nwp.precipitation", "watermark": "2026-07-11T11:50:00Z", "issued_at": "2026-07-11T11:40:00Z" }, { "segment": "future_covariates", "feature": "quarter_hour", "source_kind": "calendar", "source_ref": "calendar.utc.quarter_hour", "watermark": "2026-07-11T12:00:00Z" } ] }, "options": { "kind": "forecast", "horizon_steps": 2 }, "input_digest": "sha256:8b227777d4dd1fc61c6f884f48641d02b50a8a461a77f8fae7f48e32fbd8c372" } ``` The digest above is illustrative rather than a digest of the abbreviated example. ### Request fields | Field | Required | Validation | |-------|----------|------------| | `schema` | yes | Exactly `aether.data-processing.request.v1` | | `request_id` | yes | Correlation identity for this invocation; v1 provides no replay or de-duplication semantics | | `submitted_at` | yes | UTC time the application created the request | | `deadline` | yes | UTC time after which the processor must not start work or return an accepted result | | `task` | yes | ID, positive revision, and typed kind matching a loaded task | | `binding` | yes | Resolved commissioned binding ID and positive revision | | `processor_contract` | yes | Contract advertised by both task and processor | | `artifact` | no | Generic kind, family, and optional requested version; no path or URL | | `frame` | yes | A valid `ProcessingFrame` | | `options` | yes | A typed options object whose `kind` matches `task.kind` | | `input_digest` | yes | Canonical content identity used for correlation, audit, and offline comparison; not an operation idempotency key | `options.kind = forecast` requires positive `horizon_steps`. `quantiles` is optional; if present it contains unique finite numbers strictly between zero and one in increasing order, and its count MUST NOT exceed the commissioned task's `max_quantiles`. The current energy load and PV task revisions set that limit to zero, so their requests omit `quantiles`. A processor must reject unknown options rather than silently ignore behavior-changing fields. `input_digest` is SHA-256 over the RFC 8785 canonical JSON representation of: ```json { "task": "", "binding": "", "processor_contract": "", "artifact": "", "frame": "", "options": "" } ``` The strings in this illustration stand for the corresponding JSON values, not literal strings in the digest input. The task object includes ID, revision, and kind; the binding object includes ID and revision. The revisions make changes to their governed definitions content-distinct without copying site configuration into the processor request. Processor endpoint and identity, correlation times, and `request_id` are excluded, so independent invocations of the exact same normalized governed content retain the same digest. Repeating only `as_of` does not guarantee that content when sources are mutable. Version 1 does not provide a built-in result cache or replay store. The actor and the actor's permissions are intentionally absent. Aether authorizes the application call and audits the actor; it does not disclose identity to a processor unless a separate, explicit service-authentication protocol requires it. Artifact identity is not artifact chronology. The selector/result provenance can carry kind, family, version, and digest, but version 1 has no `trained_through` or `available_at` field and does not compare either with `frame.as_of`. A digest-pinned artifact is reproducible once supplied, yet a model trained or published later can still be selected for an old frame. Rigorous historical model evaluation MUST freeze the artifact registry at the evaluation cut until training and availability cuts become normative contract fields. ## `ProcessingResult` ### Successful forecast ```json { "schema": "aether.data-processing.result.v1", "request_id": "0190aee6-2139-7a87-8448-806f1b843201", "task": { "id": "energy.site-load-forecast", "revision": 1, "kind": "forecast" }, "binding": { "id": "site-a", "revision": 7 }, "input_digest": "sha256:8b227777d4dd1fc61c6f884f48641d02b50a8a461a77f8fae7f48e32fbd8c372", "status": "produced", "issued_at": "2026-07-11T12:00:02Z", "expires_at": "2026-07-11T13:00:00Z", "input_watermark": "2026-07-11T12:00:00Z", "processor": { "id": "load-forecasting-edge", "version": "0.1.0", "contract": "aether.data-processing.forecast.v1" }, "artifact": { "kind": "model", "family": "site-load", "version": "v3", "artifact_digest": "sha256:f04c532f2f814a3690f0f40e6f26fa82b0d69b9c510e7c0bb9f9f4de35b5a882" }, "output": { "schema": "aether.data-processing.output.forecast.v1", "kind": "forecast", "target": "load", "unit": "kW", "sign_convention": "positive_consumption", "cadence_seconds": 900, "timestamp_semantics": "interval_end", "points": [ { "timestamp": "2026-07-11T12:15:00Z", "value": 846.2 }, { "timestamp": "2026-07-11T12:30:00Z", "value": 852.7 } ] }, "warnings": [] } ``` ### Common result fields | Field | Required | Validation | |-------|----------|------------| | `schema` | yes | Exactly `aether.data-processing.result.v1` | | `request_id` | yes | Exact request correlation | | `task` | yes | Exact task ID, revision, and kind from the request | | `binding` | yes | Exact binding ID and revision from the request | | `input_digest` | yes | Exact digest from the request | | `status` | yes | `produced`, `fallback`, or `unavailable` | | `issued_at` | yes | UTC time the result was completed | | `expires_at` | produced/fallback | After `issued_at`, bounded by task policy | | `input_watermark` | yes | Exact accepted frame watermark | | `processor` | yes | Stable processor identity, version, and contract | | `artifact` | no | Actual generic artifact kind, family, version, and digest when one was used | | `output` | produced/fallback | Typed processor output; forbidden for `unavailable` | | `fallback` | fallback | Strategy, reason code, and data basis | | `unavailable` | unavailable | Reason code and retry guidance | | `warnings` | yes | Array of stable warning codes; empty when there are none | ### Forecast output Version 1 initially defines the typed forecast output schema. Estimate, detection, and classification tasks should add their own versioned output schemas instead of placing arbitrary JSON under `output`. Version 1 fixes `timestamp_semantics` to `interval_end`: every point timestamp identifies the end of the interval it forecasts. `interval_start`, `instant`, or any other interpretation requires a future contract version and MUST be rejected by a v1 decoder. A forecast MUST satisfy all of the following: - `target`, `unit`, and `sign_convention` exactly match the task declaration; - point timestamps exactly match the requested future horizon and cadence; - the number of points equals `options.horizon_steps`; - timestamps are strictly increasing and use v1 `interval_end` semantics; - values and quantile values are finite; - returned quantile probabilities exactly match the requested set; - quantile values are nondecreasing by probability at each timestamp; and - when probability `0.5` is returned, the task declares whether it must equal the primary `value` or may be a separate estimator. Aether must reject a syntactically successful processor response that violates these invariants. ## Fallback semantics Fallback is usable processor output produced by a named, approved strategy. It becomes derived data only after Aether validates and stamps it; fallback is not a way to hide a processor failure. ```json { "schema": "aether.data-processing.result.v1", "request_id": "0190aee6-2139-7a87-8448-806f1b843201", "task": { "id": "energy.site-load-forecast", "revision": 1, "kind": "forecast" }, "binding": { "id": "site-a", "revision": 7 }, "input_digest": "sha256:8b227777d4dd1fc61c6f884f48641d02b50a8a461a77f8fae7f48e32fbd8c372", "status": "fallback", "issued_at": "2026-07-11T12:00:02Z", "expires_at": "2026-07-11T12:30:00Z", "input_watermark": "2026-07-11T12:00:00Z", "processor": { "id": "load-forecasting-edge", "version": "0.1.0", "contract": "aether.data-processing.forecast.v1" }, "fallback": { "strategy": "persistence", "strategy_version": "1", "reason_code": "MODEL_UNAVAILABLE", "source_feature": "load", "based_on_data_through": "2026-07-11T11:45:00Z" }, "output": { "schema": "aether.data-processing.output.forecast.v1", "kind": "forecast", "target": "load", "unit": "kW", "sign_convention": "positive_consumption", "cadence_seconds": 900, "timestamp_semantics": "interval_end", "points": [ {"timestamp": "2026-07-11T12:15:00Z", "value": 835.0}, {"timestamp": "2026-07-11T12:30:00Z", "value": 835.0} ] }, "warnings": ["MODEL_FALLBACK_USED"] } ``` Fallback rules: - the task MUST explicitly allow the named strategy; - the response MUST use `status: fallback` and include the reason; - `expires_at` SHOULD be shorter than for normal model output; - a persistence or historical-average strategy MUST use actual request data; - a zero series is valid only when the task explicitly defines zero as its baseline and the response still labels it as fallback; and - consumers decide whether a particular fallback is acceptable. A processor cannot silently promote fallback to `produced`. For power data, zero is a plausible physical value. Returning zeros after a technical failure without a fallback label is therefore especially dangerous. ## Unavailable semantics Use `status: unavailable` when the processor handled a valid request but cannot produce any result that satisfies the task policy—for example, no approved model exists or the allowed fallback lacks enough observations. ```json { "schema": "aether.data-processing.result.v1", "request_id": "0190aee6-2139-7a87-8448-806f1b843201", "task": { "id": "energy.site-load-forecast", "revision": 1, "kind": "forecast" }, "binding": { "id": "site-a", "revision": 7 }, "input_digest": "sha256:8b227777d4dd1fc61c6f884f48641d02b50a8a461a77f8fae7f48e32fbd8c372", "status": "unavailable", "issued_at": "2026-07-11T12:00:02Z", "input_watermark": "2026-07-11T12:00:00Z", "processor": { "id": "load-forecasting-edge", "version": "0.1.0", "contract": "aether.data-processing.forecast.v1" }, "unavailable": { "reason_code": "INSUFFICIENT_HISTORY", "retryable": true, "retry_after_seconds": 900 }, "warnings": [] } ``` `output`, `expires_at`, and an apparently usable default value are forbidden in this status. ## Accepted `DerivedData` `ProcessingResult` is untrusted processor output. After all correlation, schema, range, provenance, and expiry checks pass, Aether stamps the accepted output as `DerivedData`: ```json { "schema": "aether.derived-data.v1", "result_id": "0190aee6-22ac-72da-b214-629a31ccb99c", "request_id": "0190aee6-2139-7a87-8448-806f1b843201", "task": { "id": "energy.site-load-forecast", "revision": 1, "kind": "forecast" }, "binding": { "id": "site-a", "revision": 7 }, "accepted_at": "2026-07-11T12:00:03Z", "expires_at": "2026-07-11T13:00:00Z", "input_digest": "sha256:8b227777d4dd1fc61c6f884f48641d02b50a8a461a77f8fae7f48e32fbd8c372", "processing_status": "produced", "processor": { "id": "load-forecasting-edge", "version": "0.1.0" }, "artifact": { "kind": "model", "family": "site-load", "version": "v3", "artifact_digest": "sha256:f04c532f2f814a3690f0f40e6f26fa82b0d69b9c510e7c0bb9f9f4de35b5a882" }, "quality": { "input_watermark": "2026-07-11T12:00:00Z", "missing_ratio": 0.0, "fallback_used": false }, "data": { "schema": "aether.data-processing.output.forecast.v1", "kind": "forecast", "target": "load", "unit": "kW", "sign_convention": "positive_consumption", "cadence_seconds": 900, "timestamp_semantics": "interval_end", "points": [ {"timestamp": "2026-07-11T12:15:00Z", "value": 846.2}, {"timestamp": "2026-07-11T12:30:00Z", "value": 852.7} ] } } ``` If validation fails, the application records a `rejected` outcome and does not create `DerivedData`. `rejected` is therefore an application outcome, not a status a processor may assert about its own response. ## Error envelope and HTTP mapping Contract, transport, capacity, and unexpected processor failures use a non-2xx response with a typed error. They are distinct from a completed `ProcessingResult` whose status is `unavailable`. ```json { "schema": "aether.data-processing.error.v1", "request_id": "0190aee6-2139-7a87-8448-806f1b843201", "code": "FRAME_INVALID", "category": "invalid_data", "message": "history.load contains a missing sample", "retryable": false, "details": { "path": "/frame/history/features/load/values/41", "rule": "task missing_policy is reject" } } ``` | HTTP | Category | Example codes | Retry | |------|----------|---------------|-------| | 400 | `invalid_request` | `SCHEMA_UNSUPPORTED`, `OPTION_UNKNOWN` | no | | 401/403 | `authorization` | `PROCESSOR_AUTH_REQUIRED`, `PROCESSOR_AUTH_DENIED` | only after credentials/policy change | | 404 | `not_found` | `MODEL_NOT_FOUND`, `TASK_NOT_SUPPORTED` | no unless deployment changes | | 413 | `resource_limit` | `FRAME_TOO_LARGE` | only after reducing the request | | 422 | `invalid_data` | `FRAME_INVALID`, `QUALITY_REJECTED`, `UNIT_UNSUPPORTED` | only after data changes | | 429 | `capacity` | `PROCESSOR_BUSY` | yes, honor `retry_after_seconds` | | 500 | `internal` | `PROCESSOR_INTERNAL` | policy-dependent | | 503 | `unavailable` | `MODEL_RUNTIME_UNAVAILABLE` | yes when marked retryable | | 504 | `timeout` | `DEADLINE_EXCEEDED` | yes with a fresh deadline | Error messages and details MUST NOT expose credentials, model filesystem paths, SQL statements, raw stack traces, or undeclared source data. Aether adapters map these errors into typed port errors and retain the processor's stable code for diagnostics. ## Validation order A conforming application/adapter should validate in this order: 1. size, media type, JSON syntax, and contract major version; 2. request identity, deadline, and canonical digest; 3. configured task ID, revision, kind, and processor route; 4. feature set, value types, units, and sign conventions; 5. timestamp order, cadence, window boundaries, and array lengths; 6. sample and aggregate quality against task policy; 7. optional artifact selector against the processor descriptor; and 8. result correlation, provenance, typed output schema, horizon, and expiry. Validation failures never trigger device writes and never mutate SHM. A failed processor call also cannot make history, acquisition, alarms, or deterministic safety behavior unavailable. ## Non-idempotent execution and retry `data_processing.process` is declared `idempotent: false`. Although the query does not mutate Aether state or control a device, invoking it may execute local or remote processor work and create a new required audit record. Version 1 has no replay store, request de-duplication contract, exact-result guarantee, or special `409` behavior for reused request IDs. The input digest is exact content identity, not operation identity. A caller may retry only when a typed error marks the failure retryable, and should honor retry metadata, use a fresh deadline, and assume that prior processor work may already have run. Deterministic implementations can use a pinned frame and artifact in offline golden tests; that property does not turn the public operation into an exact-replay API. ## Capability metadata Every transport that wires this capability MUST consume and expose the same application metadata. Version 1 currently exposes the application through the authenticated HTTP routes on `aether-api`; CLI and MCP bindings remain future work. The implemented baseline descriptors in the application catalog are: | Capability | Kind | Risk | Permission | Confirmation | Audit | Idempotent | |------------|------|------|------------|--------------|-------|------------| | `data_processing.tasks.list` | Query | Low | `data_processing.read` | Never | Not required | yes | | `data_processing.processors.health` | Query | Low | `data_processing.read` | Never | Not required | yes | | `data_processing.process` | Query | Medium | `data_processing.run` | Policy | Required | no | `data_processing.process` remains a query because it produces derived data and does not mutate device or Aether state. It is conservatively Medium risk with policy-driven confirmation because a configured remote processor may cause telemetry to leave the edge host. A deployment can approve a local-only route without per-call confirmation; the route's data boundary remains discoverable. Task and health discovery do not include observation values and do not require durable audit records. Processing does read task-scoped operational data, so it fails closed when the required audit sink cannot record the invocation. The application-facing routes are: - `GET /api/v1/data-processing/tasks`; - `GET /api/v1/data-processing/processors/health`; and - `POST /api/v1/data-processing/process`. They are mounted only when Data Processing is explicitly enabled. JWT role mapping grants discovery to Viewer, Engineer, and Admin; process execution is limited to Engineer and Admin. The processor-facing sidecar route remains the separate `POST /v1/process` boundary. Task bindings, routes, and approved artifacts change only through the existing governed configuration path. A processing request MUST NOT activate an artifact as a side effect. Device control is also separate and remains a High-risk command through `ControlApplication`. Task discovery returns the actual commissioned route policy. A representative entry (with the full `features` array abbreviated here) has this nested shape: ```json { "task": {"id": "energy.site-load-forecast", "revision": 1}, "binding": {"id": "energy.example-site", "revision": 1}, "kind": "forecast", "processor_contract": "aether.data-processing.forecast.v1", "features": [ { "name": "load", "role": "history", "value_type": "number", "unit": "kW", "integer": false } ], "forecast": { "target": { "name": "load", "unit": "kW", "sign_convention": "positive_consumption" }, "cadence_ms": 900000, "history_aggregation": "mean", "history_duplicate_policy": "latest", "history_feature_policies": [ {"feature": "load", "aggregation": "mean", "duplicate_policy": "latest"}, {"feature": "temp_avg", "aggregation": "mean", "duplicate_policy": "latest"}, {"feature": "humidity", "aggregation": "mean", "duplicate_policy": "latest"}, {"feature": "rain", "aggregation": "sum", "duplicate_policy": "latest"}, {"feature": "quarter_hour", "aggregation": "last", "duplicate_policy": "reject"} ], "history_steps": 672, "max_horizon_steps": 288, "max_quantiles": 0, "max_output_age_ms": 3600000, "max_missing_ratio": 0.0, "max_input_age_ms": 900000, "max_gap_ms": 1800000, "require_future_issue_time": true, "allowed_fallbacks": ["persistence"], "fallback_policies": [ { "strategy": "persistence", "version": "1", "source_feature": "load", "max_output_age_ms": 1800000 } ] }, "artifact": { "kind": "model", "family": "site-load", "version": "v3", "digest": "sha256:98967bdedc60b8ab555e596516eb272063c139ccf3a3112fb29a46ab0610f270" }, "processor_id": "load-forecasting-edge", "processor_version": "0.1.0", "data_boundary": "local", "deadline_ms": 5000, "audit_finalization_timeout_ms": 1000, "max_concurrency": 1, "max_frame_samples": 5000, "max_request_bytes": 4194304 } ``` `deadline_ms` is the hard budget for frame assembly plus processor work, not the complete HTTP response SLA. `audit_finalization_timeout_ms` publishes the separate mandatory terminal-audit allowance, so an observed API call can complete up to the sum of those two fields. Audit failure still fails the request closed. An AI client can then distinguish observing or explaining a forecast from activating a model or dispatching a control plan. ## Compatibility rules - A processor may support multiple major contracts concurrently, but each request selects exactly one. - A new task kind receives a typed options schema and a typed output schema. Do not expand Forecast fields until they become a generic blob. - Renaming a feature, changing a unit or sign convention, changing timestamp semantics, or changing a missing-data rule requires a new task revision. - Removing a compatibility adapter requires conformance tests and a stated migration criterion. - A domain pack may require a minimum processor contract, but only a composition root selects the actual adapter or endpoint. ## Related pages - [Connect Data Processors](/en/guides/data-processors) — declare a task and route a processor - [AetherEMS Power Forecasting](https://github.com/EvanL1/AetherEMS/blob/main/packs/energy/knowledge/power-forecasting.md) — first downstream forecast contract - [JSON Schemas](https://github.com/EvanL1/AetherEdge/blob/main/contracts/data-processing/README.md) — strict machine-readable v1 wire guards - [Load-Forecasting Processor](https://github.com/EvanL1/AetherEdge/blob/main/integrations/load-forecasting/README.md) — request-driven Edge-Platform implementation - [Data Flow](/en/concepts/data-flow) — SHM and history authority - [HTTP Data Processor](/en/extensions/http-data-processor) — bounded optional implementation of the v1 processor transport - [HTTP API](/en/reference/http-api) — service-envelope conventions for Aether's application-facing APIs --- # HTTP API AetherEdge runs six HTTP services, but remote applications have one network boundary: `aether-api` on port 6005. This page describes the gateway, its service-local contracts, and the security conventions that apply across it. > The OpenAPI document generated by each running service is the sole source of > truth for its paths, parameters, request and response schemas, status codes, > media types, and operation-specific security requirements. Use the matching > Swagger UI to explore and call those operations. ## Built-in documentation | Service | Default boundary | Swagger UI | OpenAPI JSON | |---|---|---|---| | `aether-io` | loopback | `http://127.0.0.1:6001/docs` | `http://127.0.0.1:6001/openapi.json` | | `aether-automation` | loopback | `http://127.0.0.1:6002/docs` | `http://127.0.0.1:6002/openapi.json` | | `aether-history` | loopback | `http://127.0.0.1:6004/docs` | `http://127.0.0.1:6004/openapi.json` | | `aether-api` | remote gateway | `http://:6005/docs` | `http://:6005/openapi.json` | | `aether-uplink` | loopback | `http://127.0.0.1:6006/docs` | `http://127.0.0.1:6006/openapi.json` | | `aether-alarm` | loopback | `http://127.0.0.1:6007/docs` | `http://127.0.0.1:6007/openapi.json` | Swagger is opt-in through each service's `swagger-ui` Cargo feature. To include it in all six services in an installer build, use: ```bash ./scripts/build-installer.sh -s rust --enable-swagger ``` When enabled, `/docs` and `/openapi.json` are public routes. Swagger does not bypass authentication on protected operations, but its schemas and the otherwise-internal API surface are visible. Enable it only on a trusted commissioning or development network. ## Exposure boundary Only `aether-api` is a remote-facing service. Its authenticated application gateway exposes five fixed namespaces and forwards them only to configured loopback services: | Remote namespace | Service-local owner | |---|---| | `/api/v1/io/*` | `aether-io` | | `/api/v1/automation/*` | `aether-automation` | | `/api/v1/history/*` | `aether-history` | | `/api/v1/uplink/*` | `aether-uplink` | | `/api/v1/alarm/*` | `aether-alarm` | The target is selected by the namespace, never by caller input. Startup validation accepts only explicit loopback HTTP origins. The gateway preserves the original signed Bearer token and the small set of documented command and conditional-request headers; it discards caller-supplied actor headers and sanitizes upstream transport errors. The direct service ports remain internal and must not be published from the device. Generated applications and downstream product interfaces use the corresponding gateway-prefixed path on port 6005. Service-local OpenAPI remains the exact operation contract during the current consolidation stage; clients translate its internal path through the fixed namespace above. This does not make the service-local port a supported client interface. A missing operation still has to be added through the owning application boundary rather than invented by a UI, attached directly to SHM, or implemented as a storage write. Loopback is a deployment boundary, not an identity credential. IO channel commissioning plus selected automation and alarm commands authenticate at the operation boundary, but many other local management routes in io, history, uplink, automation, and alarm still rely on host isolation. Do not infer that a direct service port is safe to expose because some of its operations declare a Bearer scheme. The current full channel-configuration query can include protocol parameters and per-channel logging configuration. It remains compatibility debt pending a redacted, authenticated application query capability. Keep the io port on loopback and do not proxy that response to an untrusted client. ## Authentication model `aether-api` protects its management routes with a signed access JWT. REST clients send it only in the standard header: ```http Authorization: Bearer ``` Login, refresh-token lifecycle endpoints, service health, and—when compiled in—the documentation routes form the public transport surface described by the gateway OpenAPI document. Public registration is disabled unless explicitly enabled. The required `JWT_SECRET_KEY` must contain at least 32 bytes and must be managed outside source control. The gateway WebSocket is also authenticated. Its documented `?token=...` fallback is accepted only for an actual WebSocket upgrade; REST query-string tokens are rejected. WebSocket control writes are not supported. The gateway requires an access JWT before forwarding any namespace request. The owning service then applies operation-specific authorization: - io channel create, update, delete, enable, and disable require an Admin or Engineer Bearer JWT with `io.channel.manage`; - automation device actions accept a Bearer access JWT or the dedicated `AetherService ` uplink credential; - automation rule management and manual execution require an Admin or Engineer Bearer JWT with the capability documented for the operation; - alarm rule mutation and alert resolution require an Admin or Engineer Bearer JWT; - forwarded identity headers and loopback reachability do not satisfy these protected command boundaries. ## Governed commands Commands that can change channel configuration, device, rule, processing, or alarm state declare their risk, permission, idempotency, confirmation, and audit policy in OpenAPI. The application command boundary enforces those declarations; the HTTP handler must not write SHM or storage directly. For a protected mutation, follow the operation schema exactly. Depending on the operation, explicit confirmation is carried as `x-aether-confirmed: true` or in the request body. Supply `x-request-id` when documented so retries and audit records share a stable correlation ID. Never assume that confirmation or identity may be forwarded in an undeclared header. An accepted governed command includes its `request_id` and audit outcome in the response described by OpenAPI. If dispatch or persistence succeeded but the terminal audit append failed, the operation is still accepted and its audit state is marked incomplete and non-retryable. Retain the correlation ID and do not automatically submit the command again. Failure to record the attempted audit fails closed before dispatch. Device-command acceptance means the local command plane accepted the request; it is not proof that physical equipment executed it. Use feedback telemetry for closed-loop confirmation. For channel commissioning, SQLite desired configuration and the active protocol runtime are deliberately distinct. Existing-resource mutations may document an optional `x-aether-expected-revision` compare-and-set guard. An accepted response can report an activation-pending or degraded runtime projection after desired state has committed; reconcile by `request_id` and `resulting_revision` rather than automatically repeating the non-idempotent mutation. The exact headers, receipt fields, and per-operation status codes are defined by the io Swagger document. ## Response compatibility Most business handlers return the shared success envelope: ```json { "success": true, "data": { "...": "..." }, "metadata": { "...": "..." } } ``` `metadata` is omitted when empty. Health probes, service banners, WebSocket upgrades, CSV exports, and strict Data Processing responses intentionally use their own representations. Error responses are still migrating and may use one of these compatibility shapes: - `{ "success": false, "error": { "code": 400, "message": "..." } }`; - `{ "success": false, "message": "..." }`; - the versioned Data Processing `{ "error": { "code": "...", ... } }` form; - the flat `AetherError` mapping with `error_code`, `category`, and `retryable`. Clients must treat the operation's OpenAPI status and response content type as authoritative and tolerate the documented compatibility shape. Do not infer a universal error schema from another service. ## Contributor contract When a route, schema, security rule, response, or feature gate changes, update the owning service's generated OpenAPI annotations and tests in the same change. Remote examples must use the gateway-prefixed form while local contract tests may use the owning loopback service. Do not add a second endpoint list to Markdown. Run the six-service parity check: ```bash ./scripts/check-openapi-contracts.sh ``` This check compiles the opt-in Swagger feature and verifies each service-owned Router/OpenAPI contract. It also runs in the Rust CI workflow. --- # MCP Tools Reference Generated by `scripts/gen-mcp-docs.sh` from the server's `tools/list`. 45 tools total; tools marked **WRITE** are registered only when the server runs with `--allow-write`. See [Safe Operations for Applications and Agents](/en/guides/safe-operations) before enabling writes. ## alarms ### `alarms_events` (read-only) List historical alarm events, optionally filtered by rule/type/level/keyword | Parameter | Type | Required | Description | |---|---|---|---| | `event_type` | string/null | no | Filter by event type: "trigger" (alarm raised) or "recovery" (alarm cleared) | | `keyword` | string/null | no | Keyword search across rule name, channel, point | | `level` | integer/null | no | Filter by warning level (1=low, 2=medium, 3=high) | | `page` | integer | no | Page number (1-based) | | `rule` | integer/null | no | Filter by alarm rule ID | | `size` | integer | no | Page size | ### `alarms_get` (read-only) Get a specific active alert by ID | Parameter | Type | Required | Description | |---|---|---|---| | `id` | integer | yes | Alert ID | ### `alarms_list` (read-only) List active alarms, optionally filtered by channel/level/keyword | Parameter | Type | Required | Description | |---|---|---|---| | `channel` | integer/null | no | Filter by channel ID | | `keyword` | string/null | no | Keyword search across rule name, channel, point | | `level` | integer/null | no | Filter by warning level (1=low, 2=medium, 3=high) | | `page` | integer | no | Page number (1-based) | | `size` | integer | no | Page size | ### `alarms_resolve` (**WRITE**) Resolve an active alert through the authenticated, explicitly confirmed, and audited alert-resolution application command. This high-risk command clears an operator-visible alert indication; clients must not automatically retry an incomplete audit result. | Parameter | Type | Required | Description | |---|---|---|---| | `confirmed` | boolean | yes | Explicitly confirms that the active alert indication may be cleared. | | `id` | integer | yes | Active alert ID | ### `alarms_rule_create` (**WRITE**) Create an alarm rule through the authenticated, explicitly confirmed, and audited alarm-policy application command. `body` must match the alarm CreateRuleRequest. This is a high-risk alarm-policy mutation; clients must not automatically retry an incomplete audit result. | Parameter | Type | Required | Description | |---|---|---|---| | `body` | - | yes | Full CreateRuleRequest body: service_type, channel_id, data_type, point_id, rule_name, operator, value, and optionally warning_level, enabled, description | | `confirmed` | boolean | yes | Explicitly confirms this high-risk alarm-policy mutation. | ### `alarms_rule_delete` (**WRITE**) Delete an alarm rule through the authenticated, explicitly confirmed, and audited alarm-policy application command. This is a high-risk alarm-policy mutation; clients must not automatically retry an incomplete audit result. | Parameter | Type | Required | Description | |---|---|---|---| | `confirmed` | boolean | yes | Explicitly confirms this high-risk alarm-policy mutation. | | `id` | integer | yes | Alarm rule ID | ### `alarms_rule_disable` (**WRITE**) Disable an alarm rule through the authenticated, explicitly confirmed, and audited alarm-policy application command. This is a high-risk alarm-policy mutation; clients must not automatically retry an incomplete audit result. | Parameter | Type | Required | Description | |---|---|---|---| | `confirmed` | boolean | yes | Explicitly confirms this high-risk alarm-policy mutation. | | `id` | integer | yes | Alarm rule ID | ### `alarms_rule_enable` (**WRITE**) Enable an alarm rule through the authenticated, explicitly confirmed, and audited alarm-policy application command. This is a high-risk alarm-policy mutation; clients must not automatically retry an incomplete audit result. | Parameter | Type | Required | Description | |---|---|---|---| | `confirmed` | boolean | yes | Explicitly confirms this high-risk alarm-policy mutation. | | `id` | integer | yes | Alarm rule ID | ### `alarms_rule_get` (read-only) Get a specific alarm rule by ID | Parameter | Type | Required | Description | |---|---|---|---| | `id` | integer | yes | Alarm rule ID | ### `alarms_rule_update` (**WRITE**) Update an alarm rule through the authenticated, explicitly confirmed, and audited alarm-policy application command. `body` is a partial UpdateRuleRequest. This is a high-risk alarm-policy mutation; clients must not automatically retry an incomplete audit result. | Parameter | Type | Required | Description | |---|---|---|---| | `body` | - | yes | Partial UpdateRuleRequest body -- only fields present are changed | | `confirmed` | boolean | yes | Explicitly confirms this high-risk alarm-policy mutation. | | `id` | integer | yes | Alarm rule ID | ### `alarms_rules_list` (read-only) List alarm rules, optionally filtered by channel/enabled/level/keyword | Parameter | Type | Required | Description | |---|---|---|---| | `channel` | integer/null | no | Filter by channel ID | | `enabled` | boolean/null | no | Filter by enabled/disabled state | | `keyword` | string/null | no | Keyword search across rule name, channel, point | | `level` | integer/null | no | Filter by warning level (1=low, 2=medium, 3=high) | | `page` | integer | no | Page number (1-based) | | `size` | integer | no | Page size | ### `alarms_stats` (read-only) Get aggregate alarm statistics ## channels ### `channels_create` (**WRITE**) Create a communication channel through the authenticated, explicitly confirmed, and audited io.channel.manage application command. This is a high-risk, non-idempotent commissioning mutation; enabled defaults to false. Success may report a degraded runtime projection or incomplete completion audit: inspect request_id, resulting_revision, and reconciliation_required. Clients must not automatically retry. | Parameter | Type | Required | Description | |---|---|---|---| | `confirmed` | boolean | yes | Explicitly confirms this high-risk channel commissioning mutation. | | `description` | string/null | no | Optional description | | `enabled` | boolean | no | Whether the channel starts enabled. Defaults to false so creation is inert. | | `id` | integer/null | no | Explicit channel ID; omit to auto-assign | | `name` | string | yes | Channel name | | `parameters` | - | yes | Protocol-specific connection parameters (shape depends on `protocol`) | | `protocol` | string | yes | Protocol identifier, e.g. "modbus", "iec104" | ### `channels_delete` (**WRITE**) Delete a communication channel through the authenticated, explicitly confirmed, and audited io.channel.manage application command. Action-route references cause a conflict and are never silently cascaded. This is a high-risk, non-idempotent commissioning mutation; success may report a degraded runtime projection or incomplete completion audit. Inspect request_id, resulting_revision, and reconciliation_required. Clients must not automatically retry. | Parameter | Type | Required | Description | |---|---|---|---| | `channel_id` | integer | yes | Channel ID | | `confirmed` | boolean | yes | Explicitly confirms this high-risk channel commissioning mutation. | | `expected_revision` | integer | yes | Required desired-state compare-and-set revision from the latest channel read (minimum 1) | ### `channels_disable` (**WRITE**) Disable a communication channel through the authenticated, explicitly confirmed, and audited io.channel.manage application command. This is a high-risk, non-idempotent lifecycle mutation; a degraded runtime projection is accepted and requires reconciliation. Inspect request_id, resulting_revision, and reconciliation_required. Clients must not automatically retry, including when completion audit is incomplete. | Parameter | Type | Required | Description | |---|---|---|---| | `channel_id` | integer | yes | Channel ID | | `confirmed` | boolean | yes | Explicitly confirms this high-risk channel commissioning mutation. | | `expected_revision` | integer | yes | Required desired-state compare-and-set revision from the latest channel read (minimum 1) | ### `channels_enable` (**WRITE**) Enable a communication channel through the authenticated, explicitly confirmed, and audited io.channel.manage application command. This is a high-risk, non-idempotent lifecycle mutation; activation-pending or degraded is accepted and requires reconciliation, not proof of connectivity. Inspect request_id, resulting_revision, and reconciliation_required. Clients must not automatically retry, including when completion audit is incomplete. | Parameter | Type | Required | Description | |---|---|---|---| | `channel_id` | integer | yes | Channel ID | | `confirmed` | boolean | yes | Explicitly confirms this high-risk channel commissioning mutation. | | `expected_revision` | integer | yes | Required desired-state compare-and-set revision from the latest channel read (minimum 1) | ### `channels_list` (read-only) List all configured communication channels ### `channels_mappings` (read-only) Show a channel's point-to-instance mappings | Parameter | Type | Required | Description | |---|---|---|---| | `channel_id` | integer | yes | Channel ID | ### `channels_points` (read-only) List points on a channel, optionally filtered by type (T/S/C/A) | Parameter | Type | Required | Description | |---|---|---|---| | `channel_id` | integer | yes | Channel ID | | `point_type` | string/null | no | Optional point-type filter: T \| S \| C \| A | ### `channels_points_mapping` (read-only) Show the instance mapping for a single point | Parameter | Type | Required | Description | |---|---|---|---| | `channel_id` | integer | yes | Channel ID | | `point_id` | integer | yes | Point ID | | `point_type` | string | yes | Point type: T \| S \| C \| A | ### `channels_reconcile` (**WRITE**) Reconcile every channel runtime from authoritative desired state through the authenticated, explicitly confirmed, and audited io.channel.reconcile application command. This is a high-risk, non-idempotent operation that can reconnect protocol sessions. Inspect request_id, each sanitized item, degraded_count, reconciliation_required, and completion_audit. Clients must not automatically retry, including when runtime convergence or terminal audit remains incomplete. | Parameter | Type | Required | Description | |---|---|---|---| | `confirmed` | boolean | yes | Explicitly confirms this high-risk, non-idempotent runtime reconciliation. | ### `channels_status` (read-only) Get the connection status of a specific channel | Parameter | Type | Required | Description | |---|---|---|---| | `channel_id` | integer | yes | Channel ID | ### `channels_unmapped_points` (read-only) List points on a channel that have no protocol address mapping (points not wired to a device register; instance routing is a separate concern) | Parameter | Type | Required | Description | |---|---|---|---| | `channel_id` | integer | yes | Channel ID | ### `channels_update` (**WRITE**) Update a communication channel through the authenticated, explicitly confirmed, and audited io.channel.manage application command. This is a high-risk, non-idempotent commissioning mutation; expected_revision from the latest channel read is required as a compare-and-set guard. Success may report a degraded runtime projection or incomplete completion audit: inspect request_id, resulting_revision, and reconciliation_required. Clients must not automatically retry. | Parameter | Type | Required | Description | |---|---|---|---| | `body` | - | yes | Partial update body -- only fields present are changed | | `channel_id` | integer | yes | Channel ID | | `confirmed` | boolean | yes | Explicitly confirms this high-risk channel commissioning mutation. | | `expected_revision` | integer | yes | Required desired-state compare-and-set revision from the latest channel read (minimum 1) | ## history ### `history_latest` (read-only) Get the latest historical value for a point | Parameter | Type | Required | Description | |---|---|---|---| | `point_id` | string | yes | Point ID within that series | | `series_key` | string | yes | Logical key identifying the series, e.g. "io:1001:T" | ### `history_query` (read-only) Query historical time-series data for a point over a time range | Parameter | Type | Required | Description | |---|---|---|---| | `from` | string/null | no | Start of the time range (RFC3339); omit for no lower bound | | `page` | integer | no | Page number (1-based) | | `point_id` | string | yes | Point ID within that series | | `series_key` | string | yes | Logical key identifying the series, e.g. "io:1001:T" | | `size` | integer | no | Page size | | `to` | string/null | no | End of the time range (RFC3339); omit for "now" | ## models ### `models_instances` (read-only) List device instances, optionally filtered by product type | Parameter | Type | Required | Description | |---|---|---|---| | `product` | string/null | no | Filter by product type, e.g. "ESS", "Battery" | ### `models_instances_action` (**WRITE**) Submit a control action through the authenticated, explicitly confirmed, and audited application command. Success means the local command plane accepted it, not that the physical device executed it. | Parameter | Type | Required | Description | |---|---|---|---| | `confirmed` | boolean | yes | Explicitly confirms this high-risk device command. | | `instance_id` | integer | yes | Instance ID | | `point_id` | string | yes | Numeric action point ID encoded as a string (for example, "1") | | `value` | number | yes | Value to write | ### `models_products` (read-only) List available product types ## net ### `net_cert_info` (read-only) Show installed TLS certificate info (which of ca_cert/client_cert/client_key are present) ### `net_mqtt_config_get` (read-only) Show the current uplink configuration (MQTT broker, TLS settings) ### `net_mqtt_status` (read-only) Show MQTT connection status (connected/disconnected, broker address) ## routing ### `routing_action_delete` (**WRITE**) Delete an action route through the authenticated, explicitly confirmed, and audited application command. This is a high-risk physical-topology mutation; success does not execute a device command, and clients must not automatically retry an incomplete audit or publication result. | Parameter | Type | Required | Description | |---|---|---|---| | `action_point_id` | integer | yes | Logical action-point ID within the instance model. | | `confirmed` | boolean | yes | Explicitly confirms this high-risk physical topology change. | | `instance_id` | integer | yes | Instance that owns the logical action point. | ### `routing_action_set_enabled` (**WRITE**) Enable or disable an action route through the authenticated, explicitly confirmed, and audited application command. This is a high-risk physical-topology mutation; success does not execute a device command, and clients must not automatically retry an incomplete audit or publication result. | Parameter | Type | Required | Description | |---|---|---|---| | `action_point_id` | integer | yes | Logical action-point ID within the instance model. | | `confirmed` | boolean | yes | Explicitly confirms this high-risk physical topology change. | | `enabled` | boolean | yes | Whether the route participates in command dispatch. | | `instance_id` | integer | yes | Instance that owns the logical action point. | ### `routing_action_upsert` (**WRITE**) Change the physical C/A destination of an action route through the authenticated, explicitly confirmed, and audited application command. This is a high-risk topology mutation; success does not execute a device command, and clients must not automatically retry an incomplete audit or publication result. | Parameter | Type | Required | Description | |---|---|---|---| | `action_point_id` | integer | yes | Logical action-point ID within the instance model. | | `channel_id` | integer | yes | Physical destination channel. | | `channel_point_id` | integer | yes | Physical destination point ID within the channel. | | `channel_type` | string | yes | Physical command-owned point type: C or A. | | `confirmed` | boolean | yes | Explicitly confirms this high-risk physical topology change. | | `enabled` | boolean | no | Whether the new route participates in command dispatch. | | `instance_id` | integer | yes | Instance that owns the logical action point. | ### `routing_list` (read-only) List all M2C/C2M routing entries ## rules ### `rules_create` (**WRITE**) Create a disabled business-rule shell through the authenticated, explicitly confirmed, and audited rule-management application command. This is a high-risk rule-policy mutation; clients must not automatically retry an incomplete audit result. | Parameter | Type | Required | Description | |---|---|---|---| | `confirmed` | boolean | yes | Explicitly confirms this high-risk rule-policy mutation. | | `description` | string/null | no | Optional description | | `name` | string | yes | Rule name | ### `rules_delete` (**WRITE**) Delete a business rule through the authenticated, explicitly confirmed, and audited rule-management application command. This is a high-risk rule-policy mutation; clients must not automatically retry an incomplete audit result. | Parameter | Type | Required | Description | |---|---|---|---| | `confirmed` | boolean | yes | Explicitly confirms this high-risk rule-policy mutation. | | `rule_id` | integer | yes | Rule ID | ### `rules_disable` (**WRITE**) Disable a business rule through the authenticated, explicitly confirmed, and audited rule-management application command. This is a high-risk rule-policy mutation; clients must not automatically retry an incomplete audit result. | Parameter | Type | Required | Description | |---|---|---|---| | `confirmed` | boolean | yes | Explicitly confirms this high-risk rule-policy mutation. | | `rule_id` | integer | yes | Rule ID | ### `rules_enable` (**WRITE**) Enable a business rule through the authenticated, explicitly confirmed, and audited rule-management application command. This is a high-risk rule-policy mutation; clients must not automatically retry an incomplete audit result. | Parameter | Type | Required | Description | |---|---|---|---| | `confirmed` | boolean | yes | Explicitly confirms this high-risk rule-policy mutation. | | `rule_id` | integer | yes | Rule ID | ### `rules_execute` (**WRITE**) Execute a rule now through the authenticated, explicitly confirmed, and audited application command. Selected device actions are accepted by the local command plane; success does not prove physical-device completion. | Parameter | Type | Required | Description | |---|---|---|---| | `confirmed` | boolean | yes | Explicitly confirms that the rule may dispatch real device commands. | | `rule_id` | integer | yes | Rule ID | ### `rules_get` (read-only) Get a specific business rule by ID | Parameter | Type | Required | Description | |---|---|---|---| | `rule_id` | integer | yes | Rule ID | ### `rules_list` (read-only) List all business rules ### `rules_update` (**WRITE**) Update a business rule through the authenticated, explicitly confirmed, and audited rule-management application command. `body` is the partial rule update object. This is a high-risk rule-policy mutation; clients must not automatically retry an incomplete audit result. | Parameter | Type | Required | Description | |---|---|---|---| | `body` | - | yes | Partial update body -- only fields present are changed | | `confirmed` | boolean | yes | Explicitly confirms this high-risk rule-policy mutation. | | `rule_id` | integer | yes | Rule ID | ## templates ### `templates_list` (read-only) List channel configuration templates, optionally filtered by protocol | Parameter | Type | Required | Description | |---|---|---|---| | `protocol` | string/null | no | Filter by protocol, e.g. "modbus" | ## Related pages - [Connect AI Assistants](/en/guides/ai-assistants) - [Safe Operations for Applications and Agents](/en/guides/safe-operations) - [CLI Reference](/en/reference/cli) --- # Platform status and roadmap Status is reported separately for implemented, experimental, and planned capabilities. Product names do not upgrade technical readiness. ## AetherEdge **Implemented:** six-service runtime, SHM live-state authority, embedded local operation, governed commands, `aether` CLI, `aether-edge-sdk`, Pack v1, MCP and OpenAPI foundations, and signed `v0.5.0` source/runtime/CLI artifacts. **Experimental:** CloudLink MQTT v1 edge foundation, application-ACK-driven spool, AetherContracts alpha.3 consumption, and real-Broker development evidence. **Planned or gated:** production CloudLink key lifecycle, signed ACK, complete joint conformance, legacy cutover, and remaining application-boundary migration. ## AetherCloud **Implemented foundations:** modular-monolith domain/application slices, capability-driven providers, Plan-only OpenTofu, Gateway enrollment, partial CloudLink/telemetry persistence, artifact/deployment/job foundations, audit and integration slices, observability, and a transport-neutral MCP interface. **Experimental or partial:** MQTT codec and ingress, local/AWS IoT harnesses, PostgreSQL accepted-telemetry ACK outbox, and finite audit interfaces. **Planned or gated:** production identity, complete CloudLink durability and mapping, production composition and workers, public job/deployment delivery, hardened outbound integrations, and a connectable MCP server. ## AetherContracts **Implemented, experimental:** alpha.3 specifications, closed Schemas, fixtures, TCK, digest-pinned consumer verification, and four fixture bindings. **Planned or gated:** production authentication key lifecycle, signed durable ACK, complete production codecs, and a production CloudLink cutover release. ## Platform documentation **Implemented in this migration:** shared product overview, unified navigation, deployment topologies, user journeys, end-to-end alpha tutorial, compatibility matrix, status page, and AetherIot to AetherEdge migration guide. **Planned:** custom `aetheriot.dev` and `docs.aetheriot.dev` domains, automated cross-repository version aggregation, release-channel status feeds, and a future GitHub organization when an appropriate address is available. --- # Dependency security exceptions Security advisories are denied by default. An exception is permitted only when the affected operation is unreachable, the boundary is enforced in code and a removal condition is recorded here. ## RUSTSEC-2023-0071 (`rsa`) - **Introduced by:** optional `async-opcua` support in `aether-io`. - **Risk:** the upstream RSA implementation may leak private-key information through a timing side channel. - **Runtime boundary:** `aether-io` accepts only anonymous OPC UA sessions with `SecurityPolicy::None` and `MessageSecurityMode::None`. Signing, encryption, username/password authentication and sample keypair generation are rejected before an OPC UA client is constructed. - **Default exposure:** none. The `opcua` Cargo feature is disabled by default. - **Compensating control:** use a trusted, isolated field network or a separately audited OPC UA bridge whenever transport security or authentication is needed. - **Audit policy:** both `deny.toml` and the CI/local `cargo audit` invocation ignore only `RUSTSEC-2023-0071`; every other advisory remains denied. - **Removal condition:** remove the `cargo-deny` and `cargo-audit` exceptions plus the temporary runtime restriction once `async-opcua` no longer resolves to an affected `rsa` release; then add authenticated and encrypted integration tests before re-enabling those modes. - **Review owner:** maintainers; review on every `async-opcua` update and at least once per release. This exception preserves protocol interoperability without claiming that the affected cryptographic operations are safe. --- # Connect AetherEdge to AetherCloud through AetherContracts This tutorial proves the current cross-repository integration path without claiming production CloudLink readiness. It starts with a safe local runtime, verifies the shared contract release, and then runs the available product evidence. ## 1. Select the compatible baseline Use AetherEdge `v0.5.0`, AetherContracts `v0.1.0-alpha.3`, and an AetherCloud revision that consumes the same complete contract lock. Confirm the exact combination in the [version matrix](/en/compatibility/version-matrix). Do not follow `main`, `latest`, a version range, or a sibling checkout for contract behavior. ## 2. Start AetherEdge safely Clone `EvanL1/AetherEdge`, then run the hardware-free SDK composition: ```bash cargo run -p aether-example-minimal-gateway ``` This composition commissions no device and requires no Broker or cloud service. For a supervised runtime installation, follow the [Getting Started guide](/en/guides/getting-started). ## 3. Verify the public contract authority In an AetherContracts `v0.1.0-alpha.3` checkout: ```bash pnpm test:tck ``` Then inspect each product's committed `aether-contracts.lock.json`. Both locks must name the same release tag, tag object, commit, bundle digest, manifest digest, safety policy, exact imports, and empty pending-import set. The verifier proves release distribution integrity. It does not prove a production codec, authentication system, Broker deployment, or durable cloud store. ## 4. Execute the edge contract evidence In AetherEdge, run the focused transport-neutral codec tests: ```bash cargo test -p aether-cloudlink ``` The test path validates strict input, canonical digests, replay behavior, session fencing, and current telemetry mapping without contacting a Broker. ## 5. Execute the cloud contract evidence In AetherCloud, run the default repository check: ```bash pnpm check ``` The default path validates the strict TypeScript codec, application bridge, memory and PostgreSQL adapter contracts, and documentation without requiring a database, device, Broker, or cloud account. An opt-in local dual-process Broker harness is available as development evidence: ```bash pnpm test:cloudlink-alpha-harness ``` MQTT PUBACK proves only Broker transport acceptance. AetherEdge may remove a spooled record only after the exact Cloud application acknowledgement is validated. The current alpha acknowledgement remains unsigned, and the full production crash-durable gate has not passed. ## 6. Preserve the authority boundary - Do not expose a direct point, register, SHM, or physical-control operation through CloudLink. - Do not treat a reported capability as cloud authorization. - Do not equate desired, reported, and applied state. - Do not remove the legacy path until joint authentication, durability, conformance, rollback, and support-window gates pass. The result of this tutorial is reproducible alpha integration evidence, not a production commissioning receipt.