# Conformance statement

`heartd` 0.6.0 against HEART API Standard 1.0 (draft) and HEART Core Standard 1.0 (draft).

**Claimed:** HEART Full, class Core-M, with modules `control`, `properties`, `custom` and `core`; `presence` when `[presence]` (passkey settings) is configured; and `lifecycle`, `brain`, `inventory`, `config`, `telemetry`, `diagnostics`, `software`, `artifacts`, `schedules`, `privacy`, `ai`, `memory`, `evolving`, `security`, `platform`, `provisioning`, `peers`, `access`, `batch` and `native` when their sections are configured; the Reasoning Gateway role (`POST /heart/v1/reason`) with `[reasoning_gateway]`. A module that is not configured returns `501`.

**Coverage:** every one of the 124 operations in the API Index has an implementation: 118 in the Core (served by `heartd run`, and by WASM), and `GET /heart/v1/systems` with the five `fleet` operations in `heartd gateway`. Each is offered only where its level, module or role is configured, and answers `501` elsewhere.

Claimed by the implementer and tested only by `conformance/run.py`, not independently certified.

## Implemented

| Area | Status |
|---|---|
| Basic, Connected and Full operations | Yes |
| CloudEvents envelope, published schemas | Yes, every response validated in the suite |
| Valve: levels, fail-closed, owner classifications, receipts | Yes, matches the Python vectors |
| Approval gate | Yes, all 480 combinations (risk, physical, initiator, per-initiator allowlist, mode, level) match the vectors |
| Consulting a brain: Proposals, envelopes per argument, Trials and their states, one approval per plan, learned Playbooks (Core 7.1 to 7.7) | Yes, 20 end-to-end tests in `tests/trials.rs` |
| Playbook adoption (`status`, `adoptedPhysical`, `reviewBy` on adoption) | Yes |
| A person's proof of presence with their own request: client-chosen `actionId`, one call, `409` on reuse, `403 presence-invalid` for another digest (API 6.3 rules 1 and 3) | Yes, on `invoke` and every gated operation (`PUT /properties/{name}`, `POST /lifecycle/{operation}`, `PUT /tending/autonomy`, `PUT /tending/rules/{id}`, `PUT /custom/{name}`, `PUT /brain/offline`, `POST /brain/restore`, `POST /presence/methods`); `heartd` reads `presence` on `POST`, `PUT`, `PATCH` and `DELETE` |
| The approver is never the initiator, a person included (API 6.3, Part 6 3.2) | Yes, in the Core and at the Gateway; the initiator may still deny or cancel |
| Module `presence`: list, enrol and remove passkeys (API 13.2 rule 13) | Yes for `webauthn`; enrolled passkeys persist in the Core's state and are verified beside those in the configuration (`source: "config"`), the first one through the local interface |
| One autonomy ceiling: `PUT /tending/autonomy` sets `max` 0–5, the mode follows from the current level; lowering `max` to 2 or below has the effects and scope of `POST /tending/stop`; raising it is destructive, `heart.admin`, a person only (API 13.4) | Yes |
| `/serve`: "stop the conveyor" maps to that thing's `halt`; only "stop", "stop autonomy" and the like run Stop (Core 6.2) | Yes |
| Fault codes on alerts: `code` on every alert, `kind` alongside where an old kind fits; `POST /arrhythmias` reads `code` (Part 3, Section 5) | Yes |
| Security Advisory HEART-2026-001: proof of presence, authority changes, audience and DPoP, signed Cards and Gateway headers, salted receipts | Yes (MQTT commands not applicable: no MQTT binding) |
| Ports 43278 and 7685, fallback, `system.port.fallback` | Yes |
| TLS, refusal of plain HTTP off loopback | Yes |
| Unix socket with restricted permissions | Yes |
| OAuth 2.0 access tokens | Yes, as Ed25519 JWTs |
| Server-Sent Events on Rhythm, with resume | Yes |
| Webhooks: handshake, RFC 9421 signature, clearance filtering | Yes |
| Custom definitions, injection rules, guards, interlocks, preview | Yes |
| Serve, grounding, confirmation of every change | Yes |
| Tiers Reflex and Recall | Yes |
| Tiers Local and Cloud | Through a Reasoning Gateway, which heartd can also be; tested with mock providers only |
| Stop, autonomy modes | Yes |
| Crash recovery, atomic state, damaged-state start, watchdog | Yes, tested by killing the process |
| Bounded logs and stores, drop order | Yes |
| Compiles for `wasm32-unknown-unknown` and `thumbv7em-none-eabihf` | Yes (`heart-core`), and WASM run in Node |
| Operations outside the Card's level and modules return `501` | Yes |
| `heart://` address resolution with fallback order | Yes, `heart_core::address` |
| Host feed for the product's own events | Yes: Unix socket in `heartd`, `heart_report` in WASM |
| Gateway: joining by Card, names tied to sender, ID and alias paths | Yes |
| Gateway: routed (Unix socket with verified peer user, TLS with signed caller headers) and pushed systems | Yes |
| Gateway: forwarding headers stripped and never trusted from clients | Yes |
| Gateway: its own Card, Pulse and Flatline alerts | Yes |
| Gateway: CORS for allowed browser origins, socket directory and address limits | Yes |
| Gateway: a pinned identity key moves only on a valid rotation statement signed by it (API 9.3 rule 3, 10.1 rule 7) | Yes |
| Module `inventory`: `GET /components` (parents first, paged), `/components/{id}`, `/dependencies` (API 13.2 rule 14) | Yes, from `[inventory]`; a part's `status` from its open alerts; dependencies with a `probe` are tried by heartd and raise or resolve `connectivity.dependency-unreachable` |
| Module `config`: `GET`, `PATCH` (JSON Merge Patch), `POST /config/validate`, `GET /config/history`, `POST /config/rollback` (API 13.2 rule 2) | Yes, from `[config]`: the whole result checked before approval (`400`, `422 out-of-range`, `403` above the caller's clearance or `never-share`); `write` through the gate with `actionId`/`presence`, allowlist entries `"PATCH /heart/v1/config"` with a per-setting envelope, and Stop; `Idempotency-Key` required and kept; applied by the host (`Effect::ApplyConfig`) before the revision exists; per-setting privacy through the Valve; bounded history with `gap` |
| Module `telemetry`: `GET /metrics`, `/metrics/{name}` (`from`, `to`, `step`), `/logs` (`since`, `until`, `minSeverity`, `component`, cursor, and Server-Sent Events) (API 13.2 rule 15) | Yes, from `[telemetry]`: metrics from vitals and numeric properties in bounded rings; logs from every Rhythm event, the host feed, a followed file or a journald unit, bounded, with secrets removed before they are kept |
| Module `diagnostics`: `GET /diagnostics`, `POST /diagnostics/{test}`, `POST /diagnostics/bundle` (API 13.2 rule 5) | Yes, from `[diagnostics]`: programs or native requests with time limits (`Effect::Diagnostic`); a disruptive test is `write`, through the gate, answered with its Action, cancellable and stopped by Stop; one run of a test at a time; bounded runs; bundles scoped by `since`, `until`, `include`, filtered for the caller's clearance (never above `private`) and bounded by `maxBytes` |
| Module `software`: `GET /software`, `/software/sbom`, `/software/updates`, `/software/updates/{id}`, `POST /software/updates`, `POST /software/rollback` (API 13.2 rule 4) | Yes, from `[software]`: packages and the running heartd; an SPDX or CycloneDX SBOM for `internal` callers; updates from a directory or an `https` source, each manifest's Ed25519 signature verified by the Core against `update_keys` (`signed` in the list), then the payload's size and SHA-256, before `install_command` runs; unsigned, forged or tampered updates answer `403 signature-invalid` and install nothing; installing is `destructive` (proof of presence or another person's approval), cancellable and stopped by Stop while it downloads, never while it installs; a failed install runs `rollback_command` and ends `rolled-back`; `POST /software/rollback` (`write`) returns to the version before the last update; one update at a time; bounded records |
| Module `artifacts`: `GET`, `POST /artifacts`, `GET`, `DELETE /artifacts/{id}`, `GET /artifacts/{id}/content` (API 13.2 rule 16) | Yes, from `[artifacts]`: base64 uploads with `kind`, `mediaType`, `privacy` (at most the caller's clearance, never `never-share`), optional `sha256` and `expiresAt`; files on disk, mode 0600, bounded in count, total bytes and file size; a file above the caller's clearance is not listed and answers `404`; content checked against its digest before it is sent, with `ETag` and one byte range (`206`, `416`); expired files deleted; `write` through the gate |
| Module `schedules`: `GET`, `POST /schedules`, `GET`, `DELETE /schedules/{id}` (API 6.3, 13.2 rule 17) | Yes, from `[schedules]`: `at` or five-field `cron` (UTC), a minimum interval, bounded schedules and runs; the operation and the author's scope checked first; creating has the risk of what it schedules, at least `write`, `destructive` for a maintenance window, never allowlisted; a schedule by `ai`, `peer` or `rule` is `proposed` until a person approves it; each run is a new Action with `scheduleId` that passes the gate as the author at that moment; under Stop no run that changes anything starts; late runs are `missed`; `delaySec` on a lifecycle request becomes a schedule |
| Module `privacy`: `GET`, `PUT /valve/policy`, `GET`, `POST /consents`, `DELETE /consents/{id}`, `POST /data/export`, `POST /data/erase` (API 4, 13.2 rule 6) | Yes, from `[privacy]`: the Valve policy (`llmCeiling`, classifications, `retentionDays`) is what the Valve filters with; tightening is `write`, loosening `destructive` (proof of presence or another person's approval), never allowlisted; `llmCeiling: private` needs a granted consent; private data reaches a model (and, when configured, a subscriber) only while a consent covers it, and withdrawal filters the next transfer at once; export and erasure by the person or an administrator acting for them; an export is filtered for the caller's clearance, bounded, and stored as an artifact of kind `export` with `[artifacts]`; erasure is `destructive`, reaches Memory, consents, ratings, receipt links and stored exports, writes `[erased]` in Actions, Rhythm and logs, and keeps only a record with a keyed digest |
| Module `ai`: `GET`, `PUT /ai/policy`, `GET /ai/usage` (API 7, 13.2 rule 18) | Yes, from `[ai]`: modes, providers, regions, uses, daily and monthly budgets, the most per call and fallback; any change is `destructive` (API 6.3); with `[brain]` only `org-proxy`; the policy applies to the next call (a use or provider it does not allow is answered from rules; a budget it would pass answers `429`); usage per day (calls, tokens, cost) bounded |
| Module `memory`: `GET /memory`, `POST /memory/search`, `DELETE /memory/{id}` (API 8.1, 13.2 rule 19) | Yes, from `[memory]`: messages received at the inbox (short-term, at their own level, at least `internal`) and Trial outcomes (long-term); an item above the caller's clearance is not listed, found or deleted (`404`); search by every word, best first, at most 100; bounded, expiring and within `retentionDays` |
| Module `evolving`: `PUT /evolution/pins/{purpose}`, `POST /evolution/rollback`, `GET /evolution/lessons`, `POST /feedback`; and `GET /evolution` (API 11, 13.2 rule 11) | Yes, from `[evolving]`: a pin change is `destructive`, runs the regression suite first (`Effect::Regression`, heartd's `regression_command`), and pins only if it passes; `evolving.model.adopted` or `evolving.model.rejected` in the Rhythm; the suite's Action is cancellable and stopped by Stop; rollback (`write`) returns to the previous pin once; the pin for each use is sent as `modelPolicy.pinned`; lessons are learned Playbooks; ratings bounded |
| Module `security`: `GET /security`, `GET /security/attestation`, `POST /security/rotate`, `GET /audit` (API 10.1, 13.2 rule 7) | Yes, from `[security]`: the summary per clearance; evidence signed by the identity key over the caller's `nonce` (software and digests, configuration and policy revisions), `internal` only; rotation is `destructive`, with a statement signed by the old key, `Heart-Key-Rotation` on every Card and both keys in the key set for 7 days, a second rotation refused meanwhile, kept across restarts; a bounded audit log chained by SHA-256 over RFC 8785 JSON, of every change, refused read and denied Action, without bodies |
| Module `platform`: `GET /platform` | Yes, from `[platform]` and the host feed; `serial` and `location` private |
| Module `provisioning`: `POST /provisioning/claim`, `POST /provisioning/transfer` (API 10.1 rule 3, 13.2 rule 8) | Yes, from `[provisioning]`: the code compared in constant time, 5 wrong codes lock claims for an hour (kept across restarts), the code replaced after use and written only to `claim_code_file`; the owner's key then signs the only tokens accepted; transfer with the owner's proof of presence ends the old owner's tokens at once and removes presence methods, subscriptions, consents and schedules; a factory reset makes the system claimable |
| Module `peers`: `GET /peers`, `PUT`, `DELETE /peers/{id}` (13.2 rule 20) | Yes, from `[peers]`: trust is `destructive`; blocked peers refused, clearance capped, inbox types limited; messages never start an Action |
| Module `batch`: `POST /batch` (13.2 rule 9) | Yes, from `[batch]`: each item through the same route as alone (scope, clearance, gate, its own proof, an item key from the batch's), effects awaited in turn, `stopOnError`, bounded, no nesting, the whole answer kept under the batch's key |
| Module `native`: `POST /native/{protocol}` (14.6) | Yes, from `[native]`: a person with `heart.native` and `private` clearance only, always `destructive`, `reason` required, listed protocols and targets only, read only unless `allow_writes`, never a write to a guarded or physical address, logged by digest |
| Module `access`: `GET /access/grants`, `PUT`, `DELETE /access/grants/{principal}` (API 8.2, 13.2 rule 21) | Yes, from `[access]`: the five roles as fixed scopes and clearances; granting or widening `destructive` (proof of presence or another person's approval), narrowing or removing `write`, never allowlisted, a person only; the first grant is an owner's and the last owner grant stays; expiry; bounded; every caller (token, Gateway-forwarded, command channel) held to its grant by heartd, a caller without one `peer`/`public`/no scopes; `certificate.sanUri` kept as the mutual-TLS table (`Core::certificate_caller`) |
| `POST /provisioning/release` and the table of effects (API 13.2 rule 8) | Yes: release with the owner's proof of presence removes the owner, presence methods, grants, subscriptions, consents and schedules, cancels waiting Actions and writes a new claim code to `claim_code_file`; transfer does the same for the old owner; a factory reset also removes peers and erases Memory, consents, ratings, receipt links, erasure records and every other Action, and makes a new claim code; a decommission does so and can never be claimed again; `Effect::ClaimCode` says `owner_changed` so a host renews its HEART Hub identity, and heartd makes a new Basic owner key |
| One approval for a fleet command (API 13.3, Part 6 8) | Yes: the Gateway's Action digest is the fleet digest over the command, operation, arguments, `physical`, selector, sorted targets and rollout; each system gets a new `actionId` and a `fleet` approval the Gateway signs for it; the Core accepts it only with the pinned key, as a target, for exactly that request, at most 300 s old, from someone other than the caller, and for a physical Action only with `fleet_physical`; gate, guards and Stop still apply; `presenceMethod: fleet` in the decision |
| Command channel for pushed systems (API 9.3) | Yes, with `[uplink] commands`: the Card is pushed without endpoints, long polls with `Prefer: wait`, commands signed by the Gateway key, checked for system, freshness (60 s) and replay, run through the API and gate as from the Gateway, answered as `heart.reply`; the Gateway queues, waits 30 s and relays, never decides |
| Basic credential profile: the owner key (API 8.2) | Yes, with `[auth] owner_key_file` on a Basic Card only: 256 random bits, printed once, kept as SHA-256 (mode 0600), compared in constant time, `heartd owner-key` replaces it at once, never sent anywhere |
| Polled values reach rules, Trials and metrics (Core 4.3, 7.3) | Yes: a value the Core reads itself (Modbus, `process`) is a sample for Trials' `expect` and `undoIf`, fires Tending rules that watch it, and is a metric point, as a host-feed value is |
| `PUT /tending/allowlist` (6.3, 13.2 rule 10) | Yes: widening `destructive`, narrowing `write`, never allowlisted, entries checked, kept in the state file, shown in `heart.control` |
| Reasoning Gateway: `POST /reason` (7.2) | Yes, with `[reasoning_gateway]`: OpenAI-compatible and Anthropic Messages upstreams, keys only in heartd, the Gateway's ceiling (`valve-blocked`), the pin honoured, fallback, `maxCostUsd` and a daily budget per caller (`429`), instructions apart from escaped untrusted context, the Reason format with provider, model, fallback and cost; a provider's error text is never passed on. Tested against local mock providers only |
| `Idempotency-Key` (API 5.5): required on every `POST` whose risk is not `read` (a self-test only when it is disruptive, a plain `/serve` request optional), except `acknowledge`, `resolve`, `silence` and a decision; optional on every `PUT`, `PATCH` and `DELETE` | Yes: per principal and operation, the original answer returned (a waiting Action's retry returns that Action), `422` for another body, `409` while the first still runs, released by a restart |
| The Action operations follow the Actions (API 2): `GET /actions`, `GET /actions/{id}` and the decision are offered by every system that can create an Action, whatever its level | Yes: at Connected and Full, and at Basic with any module that changes something; `501` otherwise |
| Alerts and Actions listed newest first, with a stable cursor (API 5.5) | Yes: ordered by `detectedAt` or `createdAt`, then ID; the cursor is the last item's time and ID |
| Silencing mutes notifications, not alerts (API 5.1): 60 s to 30 days; repeats raised and listed with `silencedUntil`, not notified; maintenance mutes the same way (13.2 rule 3) | Yes |
| A property above the caller's clearance answers `404`, for reading and writing; a read-only one `409 conflict` (API 5.6) | Yes, checked before the gate |
| `act` names the intermediary (API 8.2): a token whose only actors are marked HEART Gateways keeps the subject's kind; any other actor makes the caller `ai` without `heart.approve` | Yes, in heartd |
| `PUT /brain/offline`: off with `heart.tend`, on also needs `heart.admin` (13.2 rule 12) | Yes |
| A HEART Full Card lists `control` (API 2.1) | Yes: heartd refuses to start with one that does not |
| The lease renewal sends the device's `maxLeaseSec` (HEART Command Center 1.0, Section 3) | Yes |
| Event types `system.lifecycle.*` and `tending.silence.ended` (Part 3, Section 5.8) | Yes |

## Not implemented

| Requirement | Level | Note |
|---|---|---|
| Reading current values through a matching property before a Trial | MAY (Core 7.5) | The Core uses the values it last set and those the product reports with `Core::setting` |
| Native adapters other than `modbus-tcp` and `process` | MAY | The others in the standard return an error. Passthrough (module `native`) opens `modbus-tcp` only |
| Claiming by SPAKE2+ | SHOULD (10.1 rule 3) | The code travels in the request body; `local_only` keeps claims to the local interface |
| Rotating the TLS certificate through the API | MAY | `POST /security/rotate` rotates the identity key; the certificate files are the host's |
| Pinning a peer's key | MAY | A peer is known by the principal it calls with |
| Mutual TLS | MAY | OAuth with DPoP is offered instead. Grants keep the certificate table, which a host terminating mutual TLS reads with `Core::certificate_caller` |
| Renewing a HEART Hub identity after a change of owner | Module rule (13.2 rule 8) | heartd does not report to a HEART Hub; `Effect::ClaimCode` tells a host that does (`owner_changed`) |
| Destroying the identity key on decommission | Module rule (13.2 rule 8) | The host's `[lifecycle.decommission]` command does it; the Core erases and refuses any claim |
| Erasing Rhythm and audit entries on a factory reset | Module rule (13.2 rule 8) | Memory, consents, ratings, receipt links, erasure records and Actions are erased; the Rhythm and the audit chain, the system's own record, stay |
| Proof of presence by CIBA, a device button or a remote site | MAY | Only WebAuthn passkeys are verified; enrolling `ciba` answers `501` |
| Checking a passkey's attestation when it is enrolled | SHOULD | The credential's public key is taken as sent |
| Passkey signature counters across restarts | SHOULD | Kept in memory only |
| Webhook retries for 24 hours | SHOULD | Three attempts, then `suspended` |
| `traceparent` propagation | SHOULD | |
| `ETag` and `Cache-Control` on the Card | SHOULD | |
| Streaming on `/properties/{name}` | Module rule | Reading works, streaming does not |
| Failing pending Actions when a definition is deleted | Module rule | They fail at run time instead |
| Marking timestamps unsynced on a bad clock | MUST (Core 9.11) | **Gap.** Sequence numbers do continue |
| CPU ceiling inside the process | MUST (Core 9.4) | Delegated to systemd, see `packaging/` |
| Signed updates of Index and Playbooks | MUST (Core 9.15) | **Gap.** They load from local files |
| Regression tests before adopting a model without the `evolving` module | MUST at Full | Without `[evolving]`, pins come from `[model]` and cannot change through the API, so no model is adopted untested |
| Currencies other than US dollars, and days other than UTC, in the AI policy | MAY | As the standard says: US dollars and UTC |
| Consents matched by purpose | MAY | A consent covers its recipient (a kind or a name); its `purpose` is recorded |
| `requiresMaintenance` guard | Partial | Passes during maintenance: `lifecycle/maintenance-on` or a maintenance window (module `schedules`); otherwise fails closed |
| Time zones other than UTC in a schedule | MAY | Refused with `400 invalid-request` |
| Automatic update policies (`updatePolicy.auto` other than `none`) | MAY | Updates install only on request |
| Interlocks not in the form `<property> <op> <value>` | Partial | Fail closed |
| Argument schemas beyond the supported subset | Partial | Refused when the definition loads |
| `heart_core.h` C interface | Core-S | Not implemented |
| Core-S class (no allocation after startup) | Core-S | `heart-core` needs an allocator |
| Local model in the Model Slot | Optional | The interface exists, no model is bundled |
| Gateway: subscriptions on behalf of pushed systems | Gateway rule | `501`; inbox on their behalf works |
| Gateway: state is rewritten whole when it changes | Scale | Fine to a few thousand systems; a log-structured store would go further |
| Gateway: Flatline uses arrival time, not the Pulse's own `time` | Rule 5.3 | Arrival time cannot be forged by the sender, but is not what the rule says |
| Metric points and log entries across a restart | Scale | Kept in memory only; revisions and self-test runs are in the state file |
| Support bundle as an artifact (`artifactId`) | MAY | The bundle is returned in the answer, as `bundle`; module `artifacts` is not offered |
| `since` and `until` on a log stream | Module rule | A stream follows `minSeverity` and `component`; `since` and `until` apply to pages |
| A non-disruptive test that runs longer than 61.5 s | Transport | heartd answers `503`; the run still ends and is listed |

The two **gaps** mean this implementation does not yet fully meet its claimed level. They are tracked as open work.
