HEART Embedded Systems logoHEART

Changelog#

Reading tracks3 Advanced
Advanced: the reference implementation of HEART Full. Which track am I?

This project follows Keep a Changelog and Semantic Versioning.

[Unreleased]#

The four first fixes of the API audit: a person's proof of presence in one call, presence methods, fault codes on alerts, and one Stop. Then the modules inventory, config, telemetry and diagnostics (14 operations that answered 501), the modules software, artifacts and schedules (15 more), and the modules privacy, ai, memory and evolving (17 more), with a real GET /evolution. Last, the modules security, platform, provisioning, peers, batch and native, PUT /tending/allowlist, and a Reasoning Gateway role for POST /reason (14 more): every operation of the API Index now has an implementation. Then the six second-priority fixes of the audit: one approval for a fleet command, commands to pushed systems behind NAT, the Basic owner key, grants and roles, polled values reaching the rules, and the effects of every change of ownership with POST /provisioning/release.

Changed (one autonomy ceiling)#

  • GET and PUT /heart/v1/tending/autonomy (getTendingAutonomy, putTendingAutonomy) take the place of GET/PUT /tending/mode. A person sets one ceiling {"max": 0..5}; autonomy.current is the lowest of max, the promised level and what the system can do now; mode is read only and follows from current (0–2 observe, 3 suggest, 4–5 act). Lowering max needs only heart.tend and no approval, and to 2 or below it is Stop; raising it is destructive, needs heart.admin and comes only from a person (403 capability-not-allowed otherwise). Stop sets max to 2.
  • heart.control and Pulse autonomy carry max and current; the event is tending.autonomy.changed; attestation claims carry policies.autonomyMax.
  • Configuration: Config::autonomy_max and heartd [core] autonomy_max (default 5) set the starting ceiling.

Changed (consistency fixes)#

  • Action operations at every level that creates Actions (API Standard 2): GET /actions, GET /actions/{id} and the decision answer at Connected and Full, and at Basic with any module that changes something; 501 only where no Action can exist. API Index and OpenAPI give them level basic.
  • Idempotency-Key (API Standard 5.5): required on every non-read POST except acknowledge, resolve, silence and a decision (postLifecycleOperation, postBrainRestore, postArrhythmias, createSubscription, postTendingProposals, postPresenceMethods and the rest now check it); optional on PUT, PATCH and DELETE, where a key still returns a waiting Action on retry.
  • Silence (API Standard 5.1): up to 30 days (a longer one answers 400), silencedUntil set on the alerts of that code, and repeats are raised and listed but not notified. Maintenance mutes notifications the same way. DELETE removes silencedUntil.
  • Paging (5.5): alerts and Actions newest first, with a cursor of the last item's time and ID; nextCursor is set when more remain.
  • Properties (5.6): one above the caller's clearance answers 404 for reading and writing, checked before the gate; a read-only one 409 conflict, also before the gate.
  • PUT /brain/offline: the API Index scope is heart.tend; heart.admin also works, and turning auto mode on needs it (403).
  • Event names (Part 3, Section 5.8): system.lifecycle.started, .done, .failed, .cancelled, .maintenance-ended, .maintenance-window, and tending.silence.ended.
  • Native passthrough refused for ai, peer and rule answers capability-not-allowed (a caller's own request); caller-not-allowed is only for Proposals.
  • heartd: a token's act chain makes the caller ai unless every actor is a HEART Gateway marked heart_gateway: true, so a person working through a Gateway stays a person (API Standard 8.2); a HEART Full Card without the control module is refused at start; the brain lease renewal sends maxLeaseSec. The photo-editor example lists control.
  • conformance/run.py sends an Idempotency-Key on every non-read POST unless a test checks its absence.

Added#

  • One approval for a fleet command (API Standard 13.3, Part 6 Section 8). The Gateway Action's digest is the fleet digest, over {command, operation, arguments, physical, selector, targets, rollout} with the targets sorted; a person approves it once with proof of presence. Each system then gets the operation with a new actionId and presence of method fleet, signed by the Gateway for that system alone. The Core (heart_core::fleet) accepts it as the approval of its own Action only if the Gateway key it pinned signed it, the system is a target, the request is exactly the one approved, it is at most 300 seconds old, and the approver is not the caller; a physical Action also needs fleetPhysical. Its gate, guards and Stop still apply, and the decision records presenceMethod: fleet. A fleet request may say physical. heartd: [core] fleet_physical, and gateway_public_key pins the key for fleet approvals too.
  • Command channel for pushed systems (API Standard 9.3). With [uplink] commands = true, heartd pushes its Card without endpoints, long-polls the Gateway's inbox with Prefer: wait, and runs each command the Gateway signed (checked for signature, system, freshness of 60 seconds and replay) through its own API and gate, answering with a heart.reply. Forged, stale, replayed and misdirected commands are dropped unanswered and recorded as security.command.refused. The Gateway relays any request it cannot answer from what was pushed, waits up to 30 seconds, and answers 501 without a live channel.
  • Basic credential profile, the owner key (API Standard 8.2). [auth] owner_key_file on a Basic Card: heartd makes a 256-bit owner key at first start, prints it once, keeps only its SHA-256 (mode 0600), compares in constant time, and treats its holder as the owner (human, private, the owner role's scopes). heartd owner-key <config> replaces it; the old key stops at once. A Card above Basic with an owner key is refused.
  • Module access (API Standard 8.2, 13.2 rule 21): GET /heart/v1/access/grants, PUT and DELETE /heart/v1/access/grants/{principal}. Roles owner, installer, maintainer, operator and viewer, each a fixed set of scopes and a clearance (heart_core::access::ROLES). Granting or widening is destructive, narrowing or removing write, by a person only and never allowlisted; the first grant is an owner's, and the last owner grant stays. While any grant exists, heartd holds every caller to its own (Core::apply_grant); a client certificate maps through Core::certificate_caller. heartd's [access].
  • POST /heart/v1/provisioning/release (API Standard 13.2 rule 8): the owner gives the system up with proof of presence. It removes the owner, presence methods, grants, subscriptions, consents and schedules, cancels waiting Actions, and makes a new claim code shown only at the device.
  • Tests: tests/ownership.rs (7 tests: grants and roles, fleet approvals with forged, stale, misdirected, self-approved and physical cases, release, factory reset and decommission, a rule and a Trial on polled values), unit tests for roles, grant widening, fleet digests, the fleet object and channel commands; one heartd test of the new sections; two end-to-end tests in conformance/run.py (release and claim again with grants; the owner key) and three in conformance/gateway.py (one approval across three systems, routed and pushed, with forged and stale approvals refused; a command through a pushed system's channel; forged, stale, replayed and misdirected commands dropped).
  • Module security (API Standard 10.1 and 13.2 rule 7): GET /heart/v1/security (key version, identity key, owner, TLS, presence methods, update keys, audit size), GET /heart/v1/security/attestation?nonce= (evidence signed by the identity key over the caller's nonce, the software running with its digests, the configuration revision and the policy revisions; internal clearance), POST /heart/v1/security/rotate and GET /heart/v1/audit. Rotation is destructive: the host makes a new identity key and a rotation statement signed with the old one (Effect::RotateKey); for 7 days every Card carries Heart-Key-Rotation, the old key stays in the key set, and a second rotation answers 409, so no peer is cut off. The audit log is bounded and chained by SHA-256 over RFC 8785 JSON: every request that may change something, refused reads, and denied, refused or expired Actions; never a body, and a native request only by its digest. Filters category and outcome, and gap.
  • Module platform: GET /heart/v1/platform from the configuration and the host's reports of power, network and clock ({"platform": {...}} on the host feed, Core::set_platform), through the Valve.
  • Module provisioning (13.2 rule 8): POST /heart/v1/provisioning/claim and POST /heart/v1/provisioning/transfer. A claim is proved by the code from the box, compared in constant time, and runs at once; after 5 wrong codes claims answer 429 for an hour, across restarts. It records the owner's key and issuer (Core::owner_authority), and the claim code is replaced and handed to the host (Effect::ClaimCode). A transfer needs the current owner's proof of presence; it moves the authority at once and removes presence methods, subscriptions, consents and schedules, cancels waiting Actions and replaces the claim code. A factory reset makes the system claimable again.
  • Module peers (new 13.2 rule 20): GET /heart/v1/peers, PUT and DELETE /heart/v1/peers/{id}. Trusting a peer is destructive. A blocked peer is refused; a listed peer's clearance is capped by its entry (public while pending); types limits what the inbox takes from it. Messages stay data.
  • Module batch (13.2 rule 9): POST /heart/v1/batch. Each item goes through the same route as a request sent alone: scope, clearance, peer trust, the gate with its own risk, its own actionId and proof of presence, and an Idempotency-Key made from the batch's. An item that needs the host waits for its effect, and the batch goes on. At most 100 items, limits.maxBatch or the module's maxItems; stopOnError; no batch in a batch.
  • Module native (14.6): POST /heart/v1/native/{protocol}. Only a person with heart.native and private clearance; always destructive; reason required; only the protocols and targets listed; read only unless writes are turned on, and never a write to an address a custom definition marks physical or guards (409 guard-failed). process is never open.
  • PUT /heart/v1/tending/allowlist (6.3, 13.2 rule 10): widening is destructive, narrowing write, and no allowlist covers it; entries are checked (initiators, envelopes, never a destructive or physical capability). The allowlist is kept in the state file until the configured one changes, and heart.control shows it with approvalTimeoutSec.
  • Reasoning Gateway (7.2): POST /heart/v1/reason when Config::modules.reasoning is set. The Core checks the Reason request, refuses context above the Gateway's ceiling (valve-blocked), honours modelPolicy (pin, fallback, regions, maxCostUsd, maxOutputTokens), keeps a daily budget per caller, puts the owner's instructions in the system prompt and every context item and the conversation in escaped, marked blocks, and answers in the Reason format with provider, model, fallback and cost. The host adds the provider key (Effect::Reason); a provider's error text is never passed on.
  • heartd: [security], [platform] (with /etc/os-release, /proc/meminfo and a clock-sync file), [provisioning] (claim_code_file, rewritten mode 0600), [peers], [batch], [native] and [reasoning_gateway] (OpenAI-compatible and Anthropic Messages upstreams, keys from an environment variable or a file). The identity key rotates with identity.key.previous kept for the overlap and both keys in the key set; uplink pushes heartkeyrotation; the Gateway moves a pinned key on a valid rotation statement and on nothing else. Once claimed, only the owner's key signs accepted tokens. Each batch item's proof of presence is verified. In WASM, the new effects come out as attest, rotate-key, claim-code and reason.
  • Tests: tests/trust.rs (16 tests) and unit tests for widening the allowlist, claim codes and thumbprints, native reads and writes, query decoding and prompt escaping; three heartd tests (the new sections, a rotated key reloaded, and the Reasoning Gateway against a local mock provider); nine new end-to-end tests in conformance/run.py and one in conformance/gateway.py, every reply checked against the schemas.
  • Module privacy (API Standard 4 and 13.2 rule 6): GET and PUT /heart/v1/valve/policy, GET and POST /heart/v1/consents, DELETE /heart/v1/consents/{id}, POST /heart/v1/data/export and POST /heart/v1/data/erase. The Valve policy (llmCeiling, classifications, retentionDays) is the one the Valve filters with, kept in the state file until the Card's valve changes. Tightening it is write; loosening it is destructive. llmCeiling: private needs a granted consent for model-provider, and while none is granted private data does not reach a model; with consentRequired: ["subscriber"] the same holds for webhook deliveries. Withdrawing a consent filters the next transfer at once. The person, or a person with heart.admin acting for them, exports their data (Memory, consents, Actions, ratings, receipts of transfers made for them, Rhythm and log entries that name them, earlier erasures), filtered for the caller's clearance and bounded; with artifacts, as an artifact of kind export that expires. Erasing is destructive: it removes the person's Memory, consents, ratings, receipt links and stored exports, writes [erased] for their identifier in Actions, Rhythm and logs, and keeps only a record with a keyed digest of the identifier, which a later export shows.
  • Module ai (API Standard 7 and 13.2 rule 18): GET and PUT /heart/v1/ai/policy and GET /heart/v1/ai/usage. The policy's modes, providers, regions, uses, daily and monthly budgets, the most per call and fallback apply to the next model call: a use or provider it does not allow is answered from rules, a call that would pass a budget answers 429 budget-exceeded, and modelPolicy carries allowFallback, maxCostUsd, regions and the pin. Any change is destructive (API Standard 6.3); with the brain module the only mode is org-proxy. Usage per day (calls, tokens, cost, budget) is kept bounded and listed as heart.usage.
  • Module memory (API Standard 8.1 and 13.2 rule 19): GET /heart/v1/memory, POST /heart/v1/memory/search and DELETE /heart/v1/memory/{id}. Messages received at the inbox are kept as short-term items at their own level (at least internal), so inbox data can be read back; how each Trial ended is kept as a long-term item. An item above the caller's clearance is not listed, found or deleted. Bounded, expiring, and within the Valve policy's retentionDays.
  • Module evolving (API Standard 11 and 13.2 rule 11): PUT /heart/v1/evolution/pins/{purpose}, POST /heart/v1/evolution/rollback, GET /heart/v1/evolution/lessons and POST /heart/v1/feedback. A pin change is destructive and runs the system's regression suite first (Effect::Regression); the model is pinned only if it passes, and the Rhythm records evolving.model.adopted or evolving.model.rejected. The suite's Action is cancellable, and Stop cancels it. Rollback (write) returns to the previous pin once. Lessons are the Playbooks learned from Trials. Ratings of an Action, an answer, a Trial or a lesson are kept bounded. GET /heart/v1/evolution now shows the pin for each purpose with its regression result, the candidates tested and how many lessons exist; the pin for each use is sent with each model call.
  • heartd: [privacy], [ai], [memory] and [evolving] (with regression_command); heart_core::log::BoundedLog::values_mut. In WASM, the new effect comes out as regression.
  • Tests: tests/rights.rs (23 tests) and unit tests for erasing a word, loosening the Valve and widening the AI policy; the hostile-request property in tests/prop_robust.rs reaches the four modules; five new end-to-end tests in conformance/run.py, every reply checked against the schemas.
  • Module software (API Standard 13.2 rule 4): GET /heart/v1/software, /software/sbom, /software/updates, /software/updates/{id}, POST /software/updates and POST /software/rollback. The host lists what the update source offers (Effect::UpdateCheck); the Core verifies each manifest's Ed25519 signature over its RFC 8785 JSON against Config::modules.software.keys, then the downloaded payload's size and SHA-256 (Effect::UpdateDownload), and only then asks the host to install (Effect::UpdateInstall). Unsigned, forged and tampered updates answer 403 signature-invalid, raise security.update.refused and install nothing. Installing is destructive; the Action is cancellable, and stopped by Stop, until it installs. A failed install is rolled back. POST /software/rollback (Effect::Rollback) returns to the version before the last update. heartd's [software] reads an SBOM, a directory or https update source, update_keys, install_command and rollback_command, and lists itself as a package.
  • Module artifacts (API Standard 13.2 rule 16): GET and POST /heart/v1/artifacts, GET and DELETE /heart/v1/artifacts/{id} and GET /heart/v1/artifacts/{id}/content. Uploads carry the file in base64 with kind, mediaType, privacy, sha256 and expiresAt; the store is bounded in files, total bytes and file size; a file above the caller's clearance is not listed and answers 404; content is checked against its digest before it is sent, with ETag, and heartd answers one byte range with 206. The bytes live with the host (Effect::ArtifactPut, ArtifactGet, ArtifactDelete); heartd keeps them in [artifacts] dir and removes files nothing refers to at start.
  • Module schedules (API Standard 6.3 and 13.2 rule 17): GET and POST /heart/v1/schedules, GET and DELETE /heart/v1/schedules/{id}. A schedule holds one operation at a time (at) or repeatedly (five-field cron, in UTC), or declares a maintenance window, and keeps its author with their scopes and clearance. Creating it has the risk of what it schedules, at least write, and destructive for a maintenance window; no allowlist covers it; one made by ai, peer or rule is proposed until a person approves it. Each run is a new Action with scheduleId, passed through the gate as the author at that moment: a destructive run waits for a person, a peer's or AI's run follows the mode and the allowlist as they are then, and under Stop no run that changes anything starts. Runs are recorded and bounded; a run too late is missed. delaySec on a lifecycle request now becomes a schedule.
  • Idempotency-Key is required on the new writes, and accepted on every DELETE: a retry of one that waits for approval returns the same Action.
  • heart_core::codec (base64); Core::artifact_ids, Core::installed_version; heart_core::time::civil_from_days. In WASM, the new effects come out as update-check, update-download, update-install, rollback, artifact-put, artifact-get and artifact-delete.
  • Tests: tests/manage.rs (21 tests) and unit tests for cron, payload names and base64; two heartd tests of the new sections and of byte ranges; seven new end-to-end tests in conformance/run.py, with a signed update source, every reply checked against the schemas.
  • Module inventory (API Standard 13.2 rule 14): GET /heart/v1/components (parents before children, paged), GET /heart/v1/components/{id} and GET /heart/v1/dependencies, from Config::modules.inventory. A part's status comes from its open alerts. Core::dependency_status raises or resolves connectivity.dependency-unreachable; heartd's [inventory] probes each dependency's host:port.
  • Module config (API Standard 13.2 rule 2): GET and PATCH /heart/v1/config (JSON Merge Patch), POST /heart/v1/config/validate, GET /heart/v1/config/history and POST /heart/v1/config/rollback. Settings declare type, range, default and privacy; the whole result is checked before anyone approves it; every change is a revision, applied first by the host (Effect::ApplyConfig); history is bounded and says gap. Writes pass the gate (write), take actionId and presence, match allowlist entries "PATCH /heart/v1/config" with an envelope per setting, and are refused for ai, peer and rule after Stop. never-share settings are never returned or changed through the API. heartd's [config] writes a settings file and runs an apply command.
  • Module telemetry (API Standard 13.2 rule 15): GET /heart/v1/metrics, GET /heart/v1/metrics/{name} with from, to and step, and GET /heart/v1/logs with since, until, minSeverity, component and a Server-Sent Events stream. Metrics come from vitals and numeric properties, logs from every Rhythm event and from the host (Core::host_log, {"log"} on the host feed, heartd's log_file and journald_unit). Both are bounded, and secrets are removed from a line before it is kept.
  • Module diagnostics (API Standard 13.2 rule 5): GET /heart/v1/diagnostics, POST /heart/v1/diagnostics/{test} and POST /heart/v1/diagnostics/bundle. Tests run on the host (Effect::Diagnostic) with a time limit; a disruptive one is write, passes the gate, answers with its Action and is cancelled by Stop when cancellable. A support bundle takes since, until, include and maxBytes, is filtered for the caller's clearance and never holds secrets or never-share data. heartd's [diagnostics] runs programs or native requests.
  • Idempotency-Key on these writes is required, kept per principal and operation, answered with the original response, 422 for another body and 409 while the first still runs.
  • Config::modules; Core::config_values, Core::host_log, Core::dependency_status. In WASM, heart_start takes modules, and the new effects come out as config and diagnostic.
  • Tests: tests/modules.rs (22 tests); the hostile-request property in tests/prop_robust.rs reaches the four modules; three heartd tests of the new sections; eleven new end-to-end tests in conformance/run.py, every reply checked against the schemas.
  • A person's own request in one call (API Standard 6.3, rules 1 and 3). Every request that can create an Action takes actionId, a UUID the client chooses; the Action gets that ID, so the client computes the digest before sending and sends presence for it in the same request. A person's destructive or physical request with a proof that verifies runs at once (200 with the finished Action, or 202 while it runs); a proof for another digest is refused with 403 presence-invalid and creates nothing; presence without actionId is 400; a used actionId is 409 conflict; a retry with the same Idempotency-Key returns the original Action. For an operation that is not a capability, the Action's result holds the operation's own answer.
  • Module presence (API Standard 13.2 rule 13): GET, POST /heart/v1/presence/methods and DELETE /heart/v1/presence/methods/{id}. Enrolling a WebAuthn passkey is destructive and passes the gate: proved by one of the person's own passkeys in the same request, or approved by another person. While nothing is enrolled, the first passkey is proved by presence at the device: a request through the local interface (Caller::local). Listing never returns key material. Removing needs no proof, except that the last method stays when card.presence.requireMethod is set; a configured passkey is removed in the configuration. Core::passkeys, Core::seed_passkeys and Core::passkey_used serve the host.
  • heartd: passkeys in [presence] are seeded into the Core (source: "config"); enrolled ones are kept in the state file and verified too, with lastUsedAt. With [presence], the Card offers module presence and card.presence.rpId.
  • /serve: "stop the conveyor" maps to the named thing's halt capability (a capability named halt, or ending in .halt or _halt), which waits for confirmation like any physical action. Only "stop", "stop autonomy", "stop all automatic actions" and the like run Stop. A thing with no halt gets a question, never Stop.
  • Tests: tests/presence.rs (5 tests); in tests/behaviour.rs, one call with proof, PUT /tending/mode observe as Stop, halt versus Stop in /serve, and fault codes; in tests/prop_gate.rs, the property a_person_alone_runs_their_own_request_with_proof_in_one_call. The conformance suite has three new end-to-end tests and does every person's own change in one call.

Changed#

  • Polled values reach the rules (Core Standard 4.3 and 7.3): a value the Core reads itself (Modbus, process) feeds Trials' expect and undoIf, the Tending rules that watch it, and its metric, as a host-feed value does.
  • Ownership changes have one table of effects (API Standard 13.2 rule 8). A transfer also removes grants. A factory reset also removes peers, erases Memory, consents, ratings, receipt links, erasure records and every other Action, and makes a new claim code; a decommission does the same and can never be claimed. Effect::ClaimCode gains owner_changed, so a host renews its HEART Hub identity; heartd also makes a new Basic owner key. In WASM, claim-code carries ownerChanged.
  • Specification: API Standard 6.3 (presence method fleet; granting access is an authority change), 8.2 (roles, grants, the owner key), 9.3 (the command channel; a pushed system with one may claim Full), 13.1 (modules access and provisioning), 13.2 rules 8 and 21, and 13.3; Part 4 Sections 6.5, 6.6 and 9.5; Part 6 Sections 3.2, 4.1 and 8; Part 9 tests P4-OWNER-KEY, P4-ACCESS-GRANT, P4-GATEWAY-CHANNEL, P6-FLEET-ONE-APPROVAL and P6-OWNERSHIP. New message type heart.grant (grant.schema.json); fleet in the presence methods; fleetPhysical in control.schema.json; physical in fleet-request.schema.json; digest and physical in fleet.schema.json; module access. The API index has 124 operations (four new).
  • Rotating the identity key is destructive (API Standard 13.1): it changes whom every peer trusts.
  • For 7 days after a rotation a Card response carries Heart-Key-Rotation. heart.control now carries the allowlist and approvalTimeoutSec.
  • Specification: API Standard 7.2 rule 6 (a Reasoning Gateway keeps the keys and a daily budget per caller), 10.1 rule 7 (the statement's header carries the old key; the old key stays in the key set), 13.2 rules 7 to 10 (what security, provisioning, batch and the allowlist answer, and the effects of a claim, a transfer and a factory reset), new rule 20 (peers) and 14.6 rule 6 (listed protocols and targets; writes off, and never to guarded or physical addresses). The OpenAPI file gives attestation its nonce, the audit log its filters, rotation and peers their bodies, and the batch its answer; security.schema.json gains identity, owner, claimable, tls, presenceMethods, updateKeys and auditEntries, and peer.schema.json gains types and updatedAt.
  • PUT /valve/policy takes its risk from the change (API Standard 6.3, 13.1 and 13.2 rule 6): loosening is destructive, tightening write. Any change to the AI policy stays destructive, as 6.3 says. POST /data/erase no longer requires presence in its body: without it, the request waits for another person's approval, like any destructive request.
  • The daily budget, the most per call and the model pin come from the AI policy and the pins kept in the state file; the Pulse leaves out budgetUsdToday when no daily budget is set.
  • Specification: API Standard 6.3 lists loosening the Valve policy among changes to authority; 8.1, 11 and 13.2 rules 6 and 11 say how privacy and evolving answer, and new rules 18 and 19 cover ai and memory. The OpenAPI file types the bodies of putValvePolicy, postConsents, postDataExport, putAiPolicy, postMemorySearch, postEvolutionRollback and postFeedback. New message types heart.usage and heart.export (usage.schema.json, export.schema.json); aipolicy.schema.json gains revision, uses and maxCallUsd; consent.schema.json expiresAt and withdrawnAt; memory.schema.json privacy and subject, with summary taking the item's level; evolution.schema.json a pin's version and a candidate's purpose, version and requestedAt; a classification path may name brain, usage and export.
  • The approver is never the initiator, a person included (API Standard 6.3; Part 6, Section 3.2). The Core and the Gateway no longer let a person approve their own Action; they may still deny or cancel it. prop_gate.rs::a_person_never_approves_their_own_action_afterwards runs again.
  • One Stop (API Standard 13.4): PUT /heart/v1/tending/mode with observe has exactly the effects of POST /heart/v1/tending/stop, risk read and scope heart.tend; any other mode also needs heart.admin.
  • Fault codes on alerts (Part 3, Section 5): every alert carries code, and kind alongside where an old kind fits. POST /heart/v1/arrhythmias reads code, with explanation and component; a body with only an old kind is turned into its code. Silences and de-duplication go by code. A Playbook that raises an old kind raises its code. A failed undo raises system.undo-failed; the 80% budget warning raises ai.budget-near-limit; the Gateway's flatline raises system.flatline.
  • An Action for an API operation is named "METHOD /path" with the path as sent, and its arguments are the request body without actionId and presence. A physical property's write is physical.
  • heartd reads presence in the body of POST, PUT, PATCH and DELETE. The same assertion sent again for the same digest is a retry, not a replay.
  • Example rules raise dotted fault codes (connectivity.dependency-unreachable, performance.memory-exhausted, storage.write-failed).
  • The API index has 120 operations (three new).
  • An Action for a per-item operation (a lifecycle operation, a disruptive test) records the risk its request was given, write or destructive, never per-item, which action.schema.json does not allow.
  • Installing an update is destructive (API Standard 13.1), as Part 2 and Part 6 already said of installing firmware. POST /software/rollback stays write.
  • The gate's risk of an operation is worked out once, in operation_gate, which a schedule also uses for the operation it holds. An Action created by the gate keeps the initiator's scopes and clearance, for a schedule it creates.
  • Specification: API Standard 5.5 lets a DELETE carry an Idempotency-Key; 13.2 rule 4 says how software answers, new rules 16 and 17 cover artifacts and schedules, 6.3 says no allowlist covers creating a schedule, and rule 3.7 says how delaySec is gated. The OpenAPI file types the bodies of postSoftwareUpdates, postSoftwareRollback, postArtifacts and postSchedules, adds the Range header and every DELETE's optional key; update.schema.json, schedule.schema.json, software.schema.json and action.schema.json (scheduleId) gain the fields these modules return.
  • Specification: API Standard 13.2 rules 2 and 5 say how config and diagnostics answer, and new rules 14 and 15 cover inventory and telemetry. The OpenAPI file gives getLogs its filters, postConfigRollback its body, postConfigValidate its answer and postDiagnosticsBundle its optional body; diagnostic.schema.json has bundle. The API index and privacy table are rebuilt from them.

[0.6.0] - 2026-11-12#

Decision D36 of the HEART machines roadmap: the brain switch. A Command Center gives the Core its brain on a lease, signs every answer, and can take the brain away at once.

Added#

  • Module brain (API Standard 7.4, Core Standard 7.8), in heart_core::brain:
    • Leases. Core::brain_lease accepts a lease only if it is signed with the pinned Command Center key (Ed25519, card.brain.commandCenter.key), is for this system, and is newer than the last one. Its length is at most card.brain.maxLeaseSec (default 300), counted on the Core's clock from receipt, so a wrong wall clock can only shorten it, and a clock that goes back ends it.
    • Checked answers. A Proposal, or a Reason response for /ask and /serve, is used only if it is signed for a request this Core sent (brain_sent keeps their digests), under the current lease, by a model and for a use the lease names. A lease-only local model's answers are accepted unsigned, as rule 6 allows. A rejected answer is recorded as ai.answer-rejected and counted.
    • Brainless. When the lease ends, is dropped or is refused, waiting Proposals and ai Actions are cancelled, a running brain Trial is undone, and adopted Playbooks and people's own work go on. Pulse autonomy.limitedBy becomes brain-cut, capped at A4, and ai.lease-expired or ai.brain-revoked is raised.
    • Signals. Core::brain_signal and POST /heart/v1/brain/signal accept brain-off, revoke-model, revoke-use and autonomy-stop only if signed, for this system, at most 300 seconds old, and not seen before. They can only lower risk.
    • Restore. After a cut (a signal or the self-cut, not a lease that ran out), a system with restoreNeedsOwner (the default when any capability is physical) refuses leases until a person approves POST /heart/v1/brain/restore, which is destructive.
    • Auto mode. PUT /heart/v1/brain/offline is destructive to turn on and lowers risk to turn off. With the link lost (Core::brain_link(false)), a lease's offline block is used for at most its maxSec, capped by card.brain.offline.maxSec, with uses narrowing by stepDown. It ends at once when the link returns.
    • Self-cut. The Core cuts its own brain after limits.brainRefusalsPerHour (default 10) refused Proposals, or 3 rejected answers, within an hour.
    • GET /heart/v1/brain shows the state; brain_renewal, brain_renew_in_ms and brain_lease_id serve the host.
  • heartd: [brain] (command_center, key, device_id, device_key_file, max_lease_sec, restore_needs_owner, offline_max_sec).
    • It renews the lease when a third is left, passes signals and leases to the Core, and reports link loss.
    • Model calls go to /cc/v1/devices/{id}/reason. [brain] and [model] cannot both be set: no model keys on the device.
  • Tests:
    • tests/brain.rs has 16 tests.
    • tests/prop_brain.rs has 2 properties: no answer is used after its lease, or without a good signature, in any order of messages; no signal raises anything.
  • The API index has 117 operations (four new).

[0.5.0] - 2026-11-12#

Decisions D29 and D30 of the HEART machines roadmap: lifecycle requests that say how, what and when, and cancelling running work.

Added#

  • Module lifecycle (API Standard 13.2 rule 3): GET /heart/v1/lifecycle and POST /heart/v1/lifecycle/{operation} for restart, shutdown, sleep, wake, drain, reload, power-cycle, safe-mode, maintenance-on, maintenance-off, reset-config, reset-network, clear-data, factory-reset, decommission and vendor x- operations. The request is checked against lifecycle-request.schema.json before anyone is asked to approve it; unknown fields are refused.
  • The risk comes from the request and can only rise: a forced restart, a power cycle, partial and factory resets, and a shutdown of a system nothing can wake remotely (card.lifecycle.wakeSources) are destructive.
  • The Core hands a checked, approved operation to its host as Effect::Lifecycle; the Action carries nextReachableAt (or null and nextReachableNote), names the operation as "POST /heart/v1/lifecycle/<op>" in operation (D19), and a restart of the HEART or the system first undoes a running Trial.
  • Maintenance ends by itself at maintenanceUntil (durationSec required, at most limits.maxMaintenanceSec).
  • heartd: [lifecycle.<operation>] runs a program without a shell (the request in HEART_LIFECYCLE_REQUEST) or a native request; only configured operations are offered, and the Card lists them. A graceful operation that runs out of time fails; it is never forced.
  • Cancelling running work (API Standard 6.3 rule 5): cancel on an approved or running Action with cancellable: true moves it to cancelling; the host checks Core::cancel_requested at its safe points and answers {"cancelled": true}. A cancel needs no proof of presence and never undoes what was done; a running Action that is not cancellable answers 409. Stop asks running cancellable work to stop.
  • PATCH /heart/v1/subscriptions/{id} pauses and resumes a webhook; DELETE /heart/v1/arrhythmias/{id}/silence ends a silence early.
  • Action status refused counts as ended.

Fixed#

  • POST /heart/v1/arrhythmias/{id}/silence reads durationSec and silencedUntil, as the OpenAPI file says (it read only forSec).
  • Gateway fleet commands (API Standard 13.3, Part 6 Section 8): GET and POST /heart/v1/fleet/commands, GET /heart/v1/fleet/commands/{id} and POST /heart/v1/fleet/commands/{id}/{pause|resume|cancel}, with scope heart.fleet. Targets come from the selector (systems, labels, profile, status); the rollout is staged, the first stage is never more than 10% of the targets, and the defaults are 5% per stage, one failure and 300 seconds. A command that is not a GET is a Gateway Action, approved with POST /heart/v1/actions/{id}/decision and a passkey ([presence] in the Gateway configuration) by a person other than a non-human initiator. A rollout pauses at its failure limit; pause and cancel need no approval and touch no further system; resume is a new Action that needs a new approval, and its failure limit counts from the resume. Each system receives the operation as the sender, with an Idempotency-Key, and applies its own gate; a 202 from a system counts as accepted, not failed.
  • heart_core::jcs (RFC 8785) and Core::message are public, for hosts that answer some operations themselves.

[0.4.0] - 2026-11-12#

Follows the reviewed HEART Core Standard 1.0 Sections 4.3 and 7.1 to 7.7.

Security#

  • One approval covers a Trial's whole plan: the first Action's digest now includes every step's arguments (plan) and the values the undo restores (restore). Later steps and the undo carry approvedBy (the first Action) and pass the guards, not the gate.
  • The Core reads the values to restore itself (its last applied values, or Core::setting from the product). POST /heart/v1/tending/proposals refuses a body with previous (400), and a Proposal whose current values are unknown is refused 422 cannot-undo.
  • The allowlist is per initiator: entries are {target, initiators, envelope} as in heart.control. A bare string is now an entry for rule only (it used to cover ai and peer too).
  • A brain's Proposal uses the allowlist only at Autonomy Level 5 (autonomy_level, Pulse autonomy.current). In suggest every non-read Action from rule, ai or peer asks a person; in observe they are refused 409 autonomy-stopped, including a peer's.
  • A rule runs only if enabled is true and status is adopted. A rule with no status is proposed. A rule runs a physical capability only with adoptedPhysical: true (the older physical: true is still read).

Fixed (found by the property tests, TESTING.md)#

  • Action digests are SHA-256 over RFC 8785 JSON, as Part 6, Section 3.2 says: numbers are written the ECMAScript way (2.0 as 2, 1e21 as 1e+21) and keys sorted by UTF-16. They used serde_json's text, so a screen recomputing the digest got a different one for whole-number values.
  • Guards (whenStates, whenStatus, requiresMaintenance, minIntervalSec, maxPerHour) are checked just before sending, for every Action: the first step of a Trial and an Action approved later were sent without them.
  • A Trial is kept only when each expect check held by its own withinSec; before, one check could hold late as long as the longest withinSec had not passed.
  • A check never uses samples from before the step it judges, also for value: an old sample could keep a Trial.
  • A restart while a Trial is undoing keeps it undoing (it went back to running).
  • A learned Playbook's fix is refused when the Core does not know the current value of every argument it changes (D10); the undo could not have restored them.
  • In observe, a Playbook may still run a read Action; only non-read Actions are refused.
  • A PUT of a custom definition or a Playbook whose body is a JSON array, string, number or boolean is refused 400; it panicked the Core.
  • A model reply that is not a JSON object no longer panics POST /ask (it is treated as a failed call), and a negative reported cost no longer refunds the daily budget.
  • The Valve reads a dynamic field's level from its privacy sibling even when an owner classification hides that sibling (it fell back to private), and a Card view no longer gains capabilities: null.

Changed (schema alignment)#

  • Interlocks are common#/check objects ({"signal", "op", "value"}); the older text form tank.level > 10 is still read. A boolean reads as 1 or 0.
  • Pulse tending.lastAction is the id of the last Action (or null), not a capability name.
  • Card shares in the examples and the Gateway hold only message types and public/internal/private.

Changed#

  • Envelopes name each argument: {"arguments": {"<name>": {min, max, maxStep}}, maxPerDay, cooldownSec}. An argument it does not name never changes automatically. maxPerDay counts automatic Trials in any rolling 24 hours, and cooldownSec runs from the end of the last automatic Trial. Config::envelopes is gone: envelopes live in the allowlist.
  • Trial states are awaiting-approval, running, undoing, kept, undone, refused, stopped and undo-failed, as published. An undo refused by a guard ends undo-failed. Stop ends a waiting Trial stopped.
  • A Trial is kept only when every expect check held and the longest undoIf withinSec passed. Checks take windowSec.
  • Statistics: value replaces last, stddev is added, and trend is a least-squares slope per minute.
  • Proposal refusals carry the standard codes: 400 invalid-request, 403 caller-not-allowed, 404 not-found, 409 conflict, 409 autonomy-stopped, 422 out-of-range, 422 cannot-undo. Core::propose takes no previous and returns Refusal. The last step must equal the Proposal's arguments.
  • Learned Playbooks trigger on the opposite of the first expect check (a check in when), carry an envelope of the values the Trial set or restored, and get reviewBy (one year) only when a person adopts them. == and != are not learned. Playbook Trials carry ruleId and no proposalId.
  • Rules use the published form: when is a check or {event, component, count, windowSec}, and then.raise is {code, severity, summary}. Older rule files are converted on load.
  • Pulse carries autonomy. Actions carry expiresAt (approval_timeout_sec). Trials are never shown to public callers.
  • postTendingProposals is never a /serve target.

[0.3.0] - 2026-11-12#

Added#

  • POST /heart/v1/tending/proposals (scope heart.propose): a brain delivers a Proposal over the API. It becomes a gated Action with initiator ai and onBehalfOf the brain. Delivering changes nothing by itself.
  • GET /heart/v1/tending/trials/{id}: one Trial, as heart.trial.

[0.2.0] - 2026-11-12#

Security#

Meets Security Advisory HEART-2026-001 (see spec/HEART-Security-Advisory-2026-001.md in the HEART specification).

  • C1: approvals, and a person's destructive or physical actions, need a WebAuthn proof of presence over the Action's digest ([presence]). Tokens with act are treated as ai and cannot approve.
  • C2: adding a Playbook and raising autonomy are destructive. Only an administrator adopts a Playbook, and it runs only its own capability, and a physical one only if adopted as physical.
  • C3: tokens must name this system in aud. Key-bound tokens need a DPoP proof, and off loopback every token must be bound. The Gateway no longer forwards tokens: it signs caller headers (RFC 9421), and systems trust headers only when signed (gateway_public_key) or from gateway_uid on the socket.
  • C4: every system has an identity key and sends Heart-Card-Signature with its Card. The Gateway requires a signed Card to join at Connected or Full, and pins the key.
  • H1: receipts are salted commitments. The salt travels with the data.

Added#

  • Trials (Core Standard, Sections 7.1 to 7.7): Core::propose turns a brain's Proposal into a gated Action; each step and the undo are Actions; tick watches expect and undoIf; stop and restart undo a running Trial; envelopes (Config::envelopes) limit automatic changes; a kept fix becomes a proposed learned Playbook, which runs as a Trial when adopted and is suspended after two undos or at its review date. Core::consultation builds features, never samples. New module heart_core::trial; time::parse_rfc3339.
  • HEART Gateway: heartd gateway, with routed and pushed systems, Flatline alerts and persistence.
  • [uplink]: a system announces itself to a Gateway, and keeps doing so.
  • Host feed: the product reports its own events, metrics and state.
  • heart_core::address: resolves heart:// addresses.
  • WASM: heart_report, structured effects, custom definitions, rules and bring-your-own-key models.
  • Examples: air-conditioner controller, photo editor, Gateway.
  • scripts/dev-token.py for development keys and tokens.

Changed#

  • Operations outside the Card's claimed level and modules now return 501.
  • If Recall matches nothing, a model chooses among the product's own commands.

Fixed#

  • A model could pass off an invented value as a default. Defaults are now checked against the definition.

[0.1.0] - 2026-11-07#

Added#

  • heart-core: Valve, approval gate, Recall tier, serving, Tending, custom definitions.
  • heartd: HTTP over TLS and Unix sockets on ports 43278 and 7685, webhooks, Modbus TCP and process adapters, atomic state, watchdog.
  • heart-wasm: the Core for browsers.
  • Pump station example and an end-to-end conformance suite.