# HAP-1 external test-payment pilot

This package connects OpenClaw 2026.9.7 to a separate, durable HAP-1 executor.
It is a **test-mode reference integration**, not independent adoption, an audit,
a production payment service, a phone release, or proof of human presence at
every invocation. The HAP-1/1 wire format and AO-1 ticket are unchanged.
No token, Money Musk login, website or Money Musk API authorizes a payment.
The optional Stripe profile needs `api.stripe.com` for test charges.
A PaymentIntent charges a test payment method for a connected account; it does
not implement arbitrary transfers or payouts.

## Boundaries

The owner provisions an explicit bounded grant. The owner daemon holds the
human key and durable allowance/revocation state and answers fresh requests.
It is a local delegation service, not a biometric prompt. The executor alone
holds the Stripe test key. Its private control socket handles revoke delivery
and read-only reconciliation. The agent socket exposes only `challenge`,
`execute`, and `status`: no grant, raw proxy, capture, refund or retry endpoint.
Each ticket has one account, currency, audience, budget and expiry.

The Linux Docker recipe runs the whole OpenClaw Gateway with no network, a
read-only filesystem, dropped capabilities, no privilege escalation, no Docker
socket, and only delegate files plus the payment socket mounted read-only.
Owner/executor directories are absent. OpenClaw also allowlists only
`hap1_send_value` and `hap1_payment_status` and disables elevated tools.
The host administrator, executor and container runtime remain trusted.
A plugin or tool policy by itself is not this operating-system boundary.

The isolated test invokes the installed Gateway's `/tools/invoke` endpoint;
it does not call an LLM. A remote model needs a separately restricted model
transport. General internet access, host mounts, privileged flags or ungated
tools change the tested boundary.

## Reproduce

Use Node 24.19.0 for OpenClaw; core HAP-1 still supports Node 22.13+.
From the extracted archive root on Linux/macOS:

```sh
node --test scripts/hap1-payment-regressions.mjs scripts/hap1-external-regressions.mjs
HAP1_OPENCLAW_DEPS="$PWD/hap1-openclaw-deps"
mkdir -p "$HAP1_OPENCLAW_DEPS"
cp hap1/external/openclaw-runtime/package*.json "$HAP1_OPENCLAW_DEPS/"
npm ci --prefix "$HAP1_OPENCLAW_DEPS" --ignore-scripts --no-audit --no-fund
ln -s "$HAP1_OPENCLAW_DEPS/node_modules" hap1/node_modules
node hap1/node_modules/openclaw/openclaw.mjs plugins validate --root hap1 --entry ./external/openclaw.mjs
HAP1_OPENCLAW_CLI=hap1/node_modules/openclaw/openclaw.mjs node scripts/hap1-openclaw-integration.mjs
```

On Linux with Docker:

```sh
docker build -f hap1/external/Dockerfile -t hap1-openclaw-test .
node scripts/hap1-openclaw-integration.mjs --docker
```

The signed synthetic authority and recording provider exercise a USD 20 grant,
one USD 12 charge, refusal of another USD 9, replay, tampering, revoke,
unreachable authority, restart, SIGKILL after dispatch and uncertain responses.
OpenClaw tests also try forbidden tools. The Docker test tries direct internet,
host-key access and code modification. Output says `recordingProvider:true`,
`realStripe:SKIPPED` and `independentAdoption:false`.
OpenClaw's dependency lock is in `openclaw-runtime`. Installed dependencies and
the container image are excluded from the small source archive.
Install the OpenClaw host outside the `hap1` plugin package as shown above;
nesting the host installation inside its own plugin breaks source capture.

## Owner-operated Stripe test

For an automated real-provider test of this durable executor, privately set
`STRIPE_TEST_SECRET_KEY`, `STRIPE_TEST_CONNECTED_ACCOUNT`, and
`STRIPE_TEST_PAYMENT_METHOD`, then run
`node hap1/external/stripe-check.mjs`. It creates one USD 12 test charge under a
synthetic USD 20 authority, checks duplicate/overspend/revoke, and retains its
private state directory for reconciliation. Without configuration it reports
SKIPPED. The payment method must already exist in that connected test account
and have a concrete object ID (`pm_` followed by 1-128 ASCII letters or digits).
Choose/create it through your authorized Stripe test setup before running the
check. Test aliases such as `pm_card_visa` are rejected before provisioning or
network access. The check does not prove a physical human ceremony or independent adoption.

Use your own authorized Stripe test connected account and test payment method.
Never put credentials in chat, tickets, agent configuration or source control.
Live keys are rejected. Run the following from the archive root.

1. Create an exclusive new directory and explicit grant. This grants USD 20,
   at most 32 uses, for one hour. Amounts are integer minor units.

   ```sh
   node hap1/external/owner.mjs init-test /absolute/private/hap1-pilot acct_YOURTESTACCOUNT 2000 usd
   ```

2. Start the owner's authority in one terminal:

   ```sh
   node hap1/external/service.mjs human /absolute/private/hap1-pilot/owner
   ```

3. Supply `STRIPE_TEST_SECRET_KEY` privately to the executor process only and
   start it in another terminal:

   ```sh
   node hap1/external/service.mjs executor /absolute/private/hap1-pilot/executor
   ```

4. Install with `openclaw plugins install --link /absolute/path/to/hap1`.
   Configure plugin ID `hap1-payment` using the fields in `agent/client.json`.
   Allowlist only the two HAP-1 tools and apply the container restrictions above.
   `scripts/hap1-openclaw-smoke.mjs` contains the tested Gateway configuration.
   Container client paths become `/agent` and `/agent-rpc`; never mount the
   pilot's parent, owner or executor directory.

   Each logical charge uses a stable `operation` ID (16-80 ASCII letters,
   digits, `_` or `-`, e.g. a UUID), `amount`, `currency`, `payment_method`.
   Identical repeats read the recorded result; changed input is refused.
   A new ID means a different charge, never a retry of an unknown charge.

   `payment_method` must be a concrete object ID as above. Stripe's test aliases
   can resolve to different IDs, so this profile does not resolve or substitute
   them after the delegate signs. Alias inputs are refused before a challenge,
   allowance reservation, or provider request. This restriction applies to the
   durable external pilot, not the separate generic sandbox demonstration.

5. Revoke locally, then deliver the signed kill:

   ```sh
   node hap1/external/owner.mjs revoke /absolute/private/hap1-pilot
   ```

   `REVOKED_LOCAL_AND_EXECUTOR_ACKNOWLEDGED` confirms both boundaries. If delivery
   is unacknowledged, repeat when the executor is reachable. Revoke cannot undo
   a committed dispatch or reverse a charge. Every new dispatch needs fresh
   authority; there is no cached-permit fallback.

Pilot key and unlock files live in owner-managed private directories (0700;
files 0600). This reference relies on OS access control, not hardware key
isolation. The adjacent unlock file does not protect against host compromise.
Use a dedicated test machine/account; only delegate files belong in the agent.

## Outcomes and recovery

| Status | Meaning |
| --- | --- |
| REFUSED | No provider call by this executor for this operation; permission allowance may already be consumed. |
| UNKNOWN | Dispatch or its result is uncertain. Never retry automatically. |
| PROVIDER_PENDING | Matching non-final PaymentIntent returned. |
| PROVIDER_FAILED | Provider returned canceled or requires_payment_method; this executor never reconfirms it. |
| SUCCEEDED | Matching test PaymentIntent succeeded with expected amount received. This is not final bank settlement or a guarantee against refunds/disputes. |

Journal phases are `REQUESTING_PERMISSION`, `AUTHORIZED`, `DISPATCHING`,
`PROVIDER_OBSERVED`, or `REFUSED`. A permit proves authorization, not that Stripe
received a request. DISPATCHING also does not prove receipt. A matching provider
response establishes that observation. Execution receipts are signed by the
executor, not Stripe. Verify that attestation offline with
`verifyExecutionReceipt(receipt, pinnedExecutorPublicKey)` and the embedded human
permit with `verifyReceipt(permit, pinnedHumanPublicKey)`. Neither is a ZK proof.

UNKNOWN or pending blocks all new spending in this pilot. Reopening retains the
original stores; missing, empty, corrupt or wrong-identity state refuses.
SIGKILL can leave socket files: confirm the old process is dead, then remove
only those stale sockets. Never recreate SQLite databases to clear an outcome,
grant, revoke or spent allowance. Provisioning refuses an existing directory.

The executor retains final `REFUSED`, `PROVIDER_FAILED`, or `SUCCEEDED` receipts
byte-for-byte when concurrent callbacks finish. A delayed POST response or error
cannot overwrite a final reconciliation, and a delayed GET cannot overwrite a
final POST observation. An observed `PROVIDER_PENDING` result also survives a
late transport error; it still blocks spending until read-only reconciliation
resolves it. These checks run inside each journal compare-and-swap attempt.
They preserve this pilot's recorded outcomes, not a promise that a provider
object can never change outside the pilot. No retry or allowance refund is added.

Reconciliation only retrieves a PaymentIntent (GET), never creates/confirms one.
If a response was lost before recording an ID, find its ID in the authenticated
Stripe test dashboard, then run:

```sh
node hap1/external/owner.mjs reconcile /absolute/private/hap1-pilot OPERATION_ID pi_ID
```

The executor checks account context, amount, currency, exact payment-method ID,
operation, payload digest and permit digest on both POST responses and GET
reconciliation. A different, missing, null, or expanded method is not accepted:
this profile requests no expansion and requires the exact string reference.
Even a failure/cancellation response needs that binding before it can clear an
uncertain outcome. Mismatch refuses. With no usable ID, or a crash before
recorded dispatch evidence, the outcome stays UNKNOWN. Absence from search does
not prove non-payment. There is no automatic allowance refund or unknown reset.

Version 0.4.3 preserves the HAP-1 and execution-receipt signed formats and stored
receipt bytes. Existing receipts remain readable through `status` and verifiable
as historical executor observations; upgrading does not strengthen their original
checks retroactively. Existing concrete-ID operations can still reconcile. An
unresolved legacy operation using a test alias remains blocked: retain its journal
and investigate through the authenticated Stripe test dashboard. Do not rewrite
its method, erase state, refund authority, or send another POST to clear it.

The concurrency fix does not rewrite existing journal records. Recover an
existing UNKNOWN or pending operation through the same bound, read-only
reconciliation procedure above.

The payment adapter rechecks the clock after its asynchronous journal writes,
immediately before calling the provider. It requires a safe integer timestamp
at least as recent as the signed permit, effect entry and committed preparation
observations, and strictly before both the challenge and ticket expiry. Journal
updates also refuse invalid or decreasing observation times. A clock rollback
or invalid value cannot release a consumed operation or its allowance.

If the clock cannot support a new signed observation, the call can fail with
`CLOCK_ROLLBACK` while retaining the last durable `UNKNOWN` receipt. Read that
receipt through `status`; keep the journal and use the recovery procedure above.
This check covers observed clock faults at the adapter boundary. It does not
establish trusted wall time, detect an unobserved clock change, protect against
a compromised host/provider, or undo a provider call already entered. Existing
signed receipts and wire formats are unchanged.

This gives at-most-once dispatch within retained state, not universal exactly
once. A valid rollback of all local storage needs a separately retained
checkpoint/witness to detect it. Independent operator evidence, audit and
long-term key rotation remain outstanding deployment work.

Primary references:
- https://docs.openclaw.ai/plugins/tool-plugins
- https://docs.openclaw.ai/gateway/tools-invoke-http-api
- https://docs.stripe.com/api/payment_intents/create
- https://docs.stripe.com/api/payment_intents/retrieve
- https://docs.stripe.com/api/payment_intents/object
- https://docs.stripe.com/api/payment_intents/confirm
