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.