# PULSE-1 — optional local lease and Human Heartbeat

Status: reference profile in HAP-1 Live 0.9.1. No token, authority server, account,
or mandatory Money Musk domain. The project-written specification and reference are under 0BSD.
The optional QR image codec retains its [third-party licenses](vendor/qr/LICENSES.txt).
The [3D browser lab](pulse.html) uses real signatures and durable IndexedDB,
with simulated money and a simulated link in one browser. The separate paired
WebRTC adapter uses the existing local-only channel. No physical-phone, radio-range,
physical-sensor, independent-implementation or independent-audit claim is made.

PULSE-1 adds an execution requirement to selected high-risk routes. Echo Law/HAP-1
still supplies the ticket, scope, caps, exact payload binding and revocation.
Nearby still supplies transport. HORIZON-1 and personal letters keep their own
paths and need neither pulse nor heartbeat. Legacy and HAP-1/WA-1 signed bytes
are unchanged. PULSE-1 never substitutes for a configured WebAuthn ceremony.
The unrelated Legacy inheritance heartbeat (`ECHO-HEARTBEAT`) is unchanged.

“ALLOW” means the configured gate permits dispatch. It does **not** mean an act is
legally authorized. A signature authenticates a key's statement, not legal identity,
the person holding a phone, trustworthy clocks, or guaranteed biological liveness.
WebAuthn UV is stronger evidence of local user interaction; it is still not legal
identity or guaranteed biological liveness. Neither a heartbeat nor PULSE is medicine.

## Rules

1. Pay/send routes configured for PULSE-1 MUST use the dedicated wrapper and keep
   effect credentials away from the delegate. There is no caller-supplied low-risk
   label or fallback path. The operator determines which routes are high risk.
2. A valid HAP permit AND the newest valid signed pulse are required. A pulse
   opens a window of exactly 5,000 ms and can be consumed by at most one act.
   A fresh pulse does not restore a used permit, budget or revoked ticket.
3. Human Heartbeat is a second, optional requirement. When required, the newest
   pulse MUST contain a passing local observation younger than 15,000 ms at
   dispatch. No reading, failed contact, out-of-band, future or stale observation
   refuses. Sensor unavailability does not change policy.
4. The owner chooses an inclusive integer band; no “healthy” range is supplied.
   **A borrowed watch can pass.** A compromised device/adapter can fabricate data.
   These limitations remain beside the opt-in control.
5. The signed policy is immutable for a ticket. Changing the band or disabling
   heartbeat requires a new ticket; the lab first revokes the old one. Off mode
   is visibly **PULSE ONLY**. Neither mode proves a person is presently holding it.
6. The executor retains only the newest pulse and a durable counter high-water
   mark. Lower or equal counters refuse. An authentic newer failing heartbeat
   supersedes an older passing pulse. Invalid messages cannot overwrite a valid
   pulse or reset a counter.

For a registered ticket, with available state and a valid monotonic clock, refusal
priority is REVOKED, EXPIRED, FORGED, SILENT, cap/USED, then ALLOW. REVOKED and
EXPIRED refer to the trusted registered ticket, never attacker-supplied expiry or
revocation claims. Invalid/missing storage and clock rollback are additional
fail-closed errors. FORGED is a refused input, not a stored lease state.

| State | Meaning |
| --- | --- |
| LIVE | A fresh, unused lease passes the configured gates; a valid HAP permit is still needed. |
| SILENT | No matching accepted, unexpired lease, or a required heartbeat is unavailable, failed or stale. |
| USED | That pulse was consumed or the ticket's amount/use cap was reached. |
| EXPIRED | The ticket deadline passed. |
| REVOKED | An authentic revoke is retained by this executor. |

## Signed grammar

Use the unchanged HAP-1 `canonical()` rules: sorted ASCII object keys, printable
ASCII strings, exact nonnegative safe integer timestamps/counters, no floats,
negative zero, extra members, duplicate wire keys, custom prototypes or accessors.
Transport strings MUST be decoded with `parseWire()`, not permissive JSON parsing.
Hashes are SHA-256 of canonical UTF-8 JSON, including signatures where present,
encoded as 64 lowercase hex characters. Public keys are raw Ed25519, hex encoded;
issuer/executor identifiers use HAP-1 `keyId()`. Signatures are 128 lowercase hex.

For either signed object below, remove ONLY `signature` to get the body. Sign
`UTF8(body.schema + "\n" + canonical(body))` using the paired HAP issuer's key.
No trailing newline. A changed signed grammar requires a new profile version.

| Object | Exact members |
| --- | --- |
| Policy | `schema, issuer, executor, ticketHash, heartbeat, signature` |
| Pulse | `schema, issuer, executor, ticketHash, policyHash, session, counter, issuedAt, expiresAt, heartbeat, signature` |

Policy `schema` is `PULSE-1/policy`. `issuer` MUST equal the independently paired
issuer fingerprint. `executor` MUST equal the ticket audience. `ticketHash` hashes
the exact signed HAP ticket. The verifier MUST verify that ticket too.

Policy `heartbeat` has exactly `mode, source, commitment`:

- Off: `mode:"off", source:"none", commitment:` 64 zeroes.
- Required: `mode:"required", source:"ble-hr"`, a nonzero 64-hex private-band
  commitment. `source:"fixture"` is reserved for explicit tests and is rejected
  unless the executor was provisioned with `acceptTestSensors:true`. The lab
  uses the Bluetooth adapter; it has no synthetic-reading fallback.

The reference private-band commitment hashes
`{schema:"PULSE-1/private-band",band:{min,max},salt}`. Bounds are integers from
1 through 65535, min <= max, reflecting the sensor field width, not a health
recommendation. `salt` is 32 CSPRNG bytes in hex, never sent. The band is retained
only in owner-local policy storage. The commitment is not a zero-knowledge proof.

Pulse `schema` is `PULSE-1`; identity/ticket fields match the pinned policy and
`policyHash` hashes that entire signed policy. `session` is a fresh 32-byte CSPRNG
nonce, hex encoded, generated by the executor for this connection. `counter` is a
positive safe integer; the source persists a single monotonically increasing
counter, and the executor retains its maximum per ticket. Exhaustion refuses.
`expiresAt` MUST equal `issuedAt + 5000`; both times are safe Unix milliseconds.
Acceptance and dispatch require `issuedAt <= now < expiresAt`, without clock-skew
grace. HAP ticket and challenge deadlines can shorten that window further.

Pulse `heartbeat` is null in off mode. In required mode it has exactly:
`{passed:boolean, observedAt:integer, commitment:64-hex}`. Dispatch requires true
and `0 <= now - observedAt < 15000`. The reference observation commitment hashes
`{schema:"PULSE-1/private-reading",ticketHash,session,counter,reading,salt}` with a
new private random salt every time. `reading` is the owner-local
`{bpm,observedAt,contact}` or null when invalid/unavailable. The salt and reading
are discarded after hashing, never written to the executor or source store.
They are not exported. Salting avoids trivial enumeration of a low-entropy BPM.
The executor receives pass/fail, time and hash, never BPM, band, energy, RR intervals
or a waveform. This attests the owner's adapter result, not sensor truth.

The Bluetooth adapter accepts Heart Rate Service notifications, validates field
lengths and flags, and requires both supported and detected skin-contact flags.
Unsupported-contact devices fail this narrow profile. Its timestamp is the local
notification arrival time: HRS does not supply a cryptographically trusted sampling
timestamp. Cached, delayed or forged sensor readings are outside that guarantee.
The adapter never writes sensor control points, stores sensor names or IDs, or
contacts a wearable cloud. Sources:
[Bluetooth SIG HRS 1.0](https://www.bluetooth.com/wp-content/uploads/Files/Specification/HTML/HRS_v1.0/out/en/index-en.html)
and [Chrome Web Bluetooth](https://developer.chrome.com/docs/capabilities/bluetooth).

## Offline transport and revocation

Internet availability, radio reception and lease freshness are separate facts.
No internet/server check is part of authorization. Radio silence stops new
reception; it does not move the deadline of an already accepted pulse.

| Situation | Result, assuming all ticket/permit/cap/heartbeat checks pass |
| --- | --- |
| Both devices offline, local link available | The owner can issue a pulse and exact-action permit locally; the executor may dispatch. |
| Radio lost, accepted pulse is 2 seconds old | The executor may consume that pulse once with an already issued exact-action permit, while both deadlines remain open. |
| Accepted pulse is exactly 5 seconds old, or 40 seconds old | SILENT. No act, automatic retry, or delayed synchronization can repair the missed window. |

`disconnect()` synchronously closes reception and durably records radio silence.
It retains only the previously accepted pulse, its original deadline and its
one-use status. A late response cannot be accepted after reception closes.
`stop()` is an explicit local stop: it immediately fences pending callbacks and
clears the session/lease. It is stronger than losing the radio, and is used by
the lab's stop and revoke controls. An authenticated revoke remains separate.

Initial connection, restart and explicit reconnect create a new session nonce
and conservatively invalidate old leases. Reconnect alone cannot restore LIVE;
a newly signed and accepted pulse is required. Counter maxima, caps, clock
high-water marks and kills survive normal restarts. Backgrounding the lab pauses
pulses, closes reception and closes the sensor; leaving the page stops the local
gate. Timers, connectivity badges and animation never authorize an act.

`pulse-peer.mjs` runs over the existing HAP phone channel: trusted comparison of
the six-group code on both ends, issuer/delegate/executor key pins, no STUN/TURN,
no signaling server, no remote grant. It uses `sync` with exact request
`{schema:"PULSE-1/hear",ticketHash,session}` and returns a signed pulse. Other
requests refuse; the peer accepts authentic `revoke` deliveries. Call `hear()`
while connected (for example every two seconds). A failure/disconnect closes
reception; a cached lease ages out at its original deadline. There is no transport
fallback. The owner provisions the
ticket and policy before pairing. Key/session pinning does not prove physical
distance or prevent a cooperating relay. This adapter does not alter the native
iOS Nearby implementation or automatically activate in the legacy phone lab.

The versioned [offline QR transport](PULSE-QR.md) implements executor-signed
ephemeral challenges and unchanged signed phone pulses, with a [mobile lab](pulse-qr.html)
and hostile tests. It preserves the gate, pins, counters and exclusive deadlines;
a scan never grants authority or proves proximity/presence. Physical-device QR
behavior has not been field-validated. Bluetooth and NFC phone-to-agent PULSE
transports remain unimplemented. The Bluetooth adapter reads a local sensor;
it is not a phone-to-agent PULSE link.

Offline, an accepted pulse and all other mandatory checks can permit a local
dispatch, including during the remaining window after radio loss. Actual
payment-provider availability is separate;
PULSE does not make a remote payment settle without a network. A device signature
and its time field do not independently prove when signing physically occurred.
The original HAP unpredictable executor challenge and fresh owner permit remain
mandatory for every act; leases cannot authorize precomputed future actions alone.

A queued intent is not a payment. Prepare its exact-action permit while the owner
is reachable locally. Its HAP deadline can be earlier than the pulse deadline;
both are enforced at the final dispatch boundary. If either expires, reading or
syncing receipts does not execute the intent, extend a deadline, or restore budget.
Even a new pulse cannot revive an expired permit: explicitly obtain a new owner
permit for a new executor challenge. The lab's prepared intent is memory-only,
reserves budget without refund and is never automatically submitted. Reload drops
it. “Pay tonight while I sleep” belongs to a separately authorized bounded-ticket
policy with its own expiry, never an extended PULSE lease or an automatic bypass.

Reading one's own letter remains outside the execution gate. Any low-risk act
allowed without a pulse requires an explicit trusted operator route; caller
labels cannot downgrade pay/send or toggle a required heartbeat off.

Stop the owner source at a revoke tap, issue the normal HAP revoke, and deliver it
to every executor. Show “acknowledged” only after `executor.revoke()` completes.
A revoke received and ordered before the final dispatch check wins over fresh
pulse/heartbeat. The in-process fence also stops callbacks already preparing an
act. Neither a radio-off executor nor an external provider can observe a tap that
has not reached it, and a completed dispatch cannot be undone by this profile.

## Integrating the wrapper

Trusted owner provisioning creates `createPulseSource({identity,store,initialize,
readHeartbeat})`. Call `enroll(ticket,{band})` with null band for off mode, or the
chosen private band for required mode. Keep the source, store, identity and sensor
callback out of delegate IPC. Source `revoke(ticketHash)` stops pulse issuance;
it does not replace the normal authority revoke or its delivery.

Trusted executor provisioning creates `createPulseExecutor({identity,
issuerPublicKey,store,initialize})`, then `register(ticket,policy)`. Use a separate,
durable profile record. `initialize:true` is for first provisioning only, with a
separately retained marker; missing state under an existing marker must refuse.
Never expose a raw legacy executor or provider credentials beside this wrapper.
Each wrapper guards every ticket routed through it. Letters stay on their separate
existing routes. Never let caller payload labels select an unguarded payment path.

```js
const session = await gate.connect(); // authenticated local transport is ready
await gate.accept(ticket, await source.pulse(await hash(ticket), session));
const challenge = await gate.challenge(ticket, actionBoundToExactPayload);
const request = await signRequest(challenge, delegateIdentity);
// Use the configured owner authority, including WA-1 if required. No fallback.
const bundle = await requestConfiguredPermit(ticket, request);
await gate.execute(ticket, bundle, payload, async (body, permit) => {
  await prepareDurableOperation(body); // reversible preparation only
  return permit.dispatch(() => providerSend(body)); // no await before providerSend
});
```

`execute()` is a preparation callback, **not** permission to perform effects
outside `permit.dispatch()`. Dispatch is a one-use callback capability, valid
only during that invocation. The wrapper waits for it and rechecks durable kill,
session, newest counter, pulse/heartbeat, ticket and permit deadlines after
preparation. The adapter must derive the HAP action from the same payload and
operator-pinned route. A provider call with its own internal asynchronous queue
needs a corresponding check at that provider's actual irreversible boundary.

The wrapper runs the unchanged core on a private snapshot, validates the permit,
then atomically consumes that challenge, pulse and local budget in one CAS.
Optimistic retries never invoke effects. Failures after consumption do not refund;
unknown provider outcomes must be reconciled read-only. Authenticated revokes,
including other tickets' revokes in a refused bundle, and clock high-water marks
are retained. Delayed storage acknowledgements cannot extend a window.

The final synchronous check also observes a per-ticket pulse fence and every
revoke authenticated by the private HAP verifier. A newer, correctly bound and
fresh pulse fences an older pending callback **before** waiting for the acceptance
write. That includes a newer failed heartbeat. A revoke learned through another
ticket's permit bundle wins even if that other action is refused. Neither event
can slip through a delayed final-check acknowledgement. Forged, stale, replayed
and wrong-session pulse inputs do not change the pulse fence; a different ticket's
pulse does not cancel this ticket. A consumed claim stays consumed after refusal.

If the newer pulse's write fails or a CAS attempt loses, an older pending callback
may be conservatively refused. If a revoke write fails, the live process still
remembers the authenticated kill, but it MUST NOT report durable acknowledgement.
Retry delivery and obtain a successful durable acknowledgement before relying on
that kill across a restart. These local fences require all pulse acceptance,
revocation and effect dispatch for an executor to pass through the same live gate.
Separate workers must route through that gate or provide equivalent coordination
at the effect boundary; sharing a database alone does not broadcast an in-memory
fence. Give independently operating executors separate identities and tickets.

`createPaymentExecutor({executor:gate, requirePulse:true, ...})` rejects a legacy
executor and uses `permit.dispatch()` after its journal writes. The existing
OpenClaw service remains opt-in/unchanged; it is not silently upgraded by installing
this version. Deployment operators must provision the wrapper and authenticated
link in their own service. A failing final check can leave an already prepared
operation conservatively UNKNOWN; it never retries the provider POST.

Both stores use atomic read/CAS, durable monotonic counters and observed clock
high-water marks. Normal restart is covered; restoring an old database, clearing
the entire browser identity/marker, malicious clocks/OS, compromised owner/executor
code and storage rollback need independently retained checkpoints or stronger
hardware/storage. No software-only profile claims to solve those threats.

## Reproduce verification

The [accepted-lease inspection kit](PULSE-INTEROP.md) adds a versioned, read-only
contract, 110 shared hostile fixtures and a separate Python verifier. It does not
replace this stateful executor or establish independent adoption. Golden signed
bytes and the complete corpus are packaged with the source.
The [external adapter runner](PULSE-ADAPTER.md) tests another implementation and
writes an inspection-only report; it cannot replace the executor gate.

```sh
node --test scripts/hap1-pulse-regressions.mjs
HAP1_INDEXEDDB_MODULE=/absolute/path/to/fake-indexeddb/build/esm/index.js \
  node --test scripts/hap1-pulse-browser-contract.mjs
HAP1_PLAYWRIGHT_MODULE=/absolute/path/to/playwright/index.mjs \
  node scripts/hap1-pulse-browser.mjs
python3 scripts/build-hap1.py --check
```

Use the CI-pinned fake-indexeddb and Playwright versions. Tests cover tampered
signatures/bindings, issuer substitution, counters, deadlines, heartbeat policy,
privacy, concurrent consumption, source revocation during sensor reads, kills
during effect preparation, delayed/failed storage, SQLite/IndexedDB restart,
missing state, browser cancellation and provider dispatch. Offline cases include
cached acceptance at 2 seconds, refusal at the exclusive 5-second boundary and
40 seconds, one-use/counter retention, radio loss during preparation, explicit
stop, received revoke, and expired intent refusal after receipt inspection and
reconnect. The Chromium suite actually disables browser networking while local
signing and prepared dispatch run; the radio-range interaction remains simulated.
The Chromium suite
exercises real DOM, IndexedDB and same-host ICE/DTLS with a clearly synthetic
sensor. Delayed IndexedDB acknowledgement tests cover newer passing/failing
pulses and a revoke arriving in another refused bundle: zero effect calls and no
budget refund. Tests also cover forged-input isolation, unrelated tickets and
failed durable writes. It verifies mobile/desktop layout and reduced motion. This is project
conformance evidence, not independent review, a medical test or field validation.

Before an independent deployment, document the high-risk route mapping, trusted
key ceremony, sensor provenance, physical-device behavior, disconnect detection,
clock/storage assumptions and final provider boundary. Reuse the review discipline
in [PRESENCE-REVIEW.md](PRESENCE-REVIEW.md); WA-1 corpus results are not PULSE-1
compatibility results. `verifyPulse()`/`verifyPulsePolicy()` are historical
cryptographic inspection helpers and never authorize an act by themselves.
