# Changelog

This project follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [Semantic Versioning](https://semver.org/).

## [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.
