Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
219 changes: 219 additions & 0 deletions docs/runbooks/mercury-payment-executor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
---
canonicalUri: chittycanon://docs/runbooks/chittycommand/mercury-payment-executor
service: chittycommand
component: meta/executors/mercury-payment
risk: real-money
---

# Mercury Payment Executor — Operator Runbook

**REAL MONEY PATH.** This runbook covers the autonomous-execution path for
Mercury ACH payments via the meta-orchestrator. The executor lives at
`meta/executors/mercury-payment.ts` and is dispatched by
`meta/executors/dispatch.ts` when a `mercury_payment` intent reaches the
daemon loop.

Refer to ADR-001 (and its amendment) at
`docs/architecture/ADR-001-meta-orchestrator-extension.md` for the
architectural context.

## What the executor refuses

The executor refuses (writes a `payment_refusal` row to `cc_actions_log`
and does NOT call Mercury) in any of these cases:

| Refusal reason | Trigger |
|---------------------------|-------------------------------------------------------------------------|
| `sovereignty_not_autonomous` | `sovereignty_assessment.decision !== 'autonomous'` |
| `sovereignty_stale` | snapshot `assessedAt` older than 60s at executor entry |
| `amount_cap_exceeded` | `payload.amount_cents > MERCURY_AUTONOMOUS_AMOUNT_CAP_USD * 100` |
| `invalid_payload` | zod schema rejected the payload |
| `missing_token` | `mercury:token:{account_slug}` is absent from `COMMAND_KV` |
| `mercury_api_failure` | Mercury HTTP call returned null (5xx, timeout, network) |

Successful executions write a `payment` row with `status='completed'`.

## Sovereignty configuration

The executor requires:

1. The sovereignty assessment on the intent (set by `createIntent` or
computed by dispatch's re-reckon) MUST have `decision: 'autonomous'`.
`requires_human` and any blocked state both refuse this money-path
executor — even `requires_human` is treated as a refusal here.
2. The snapshot's `assessedAt` MUST be within
`MERCURY_SOVEREIGNTY_FRESHNESS_MS` (60 seconds). Tighter than the
default 5 minutes for non-money paths.

The daemon / orchestrator that invokes `executeIntent` for a
`mercury_payment` intent SHOULD pass:

```ts
import { MERCURY_SOVEREIGNTY_FRESHNESS_MS } from '../meta/executors/mercury-payment';

await executeIntent(env, intentId, {
actorChittyId: ownerChittyId,
freshnessMs: MERCURY_SOVEREIGNTY_FRESHNESS_MS,
});
```

This forces the dispatcher to re-reckon against `trust.chitty.cc` if the
snapshot on the intent is older than 60s. The executor's own freshness
check is a belt-and-suspenders safety net for that same window.

To allow an autonomous `mercury_payment`, the owner ChittyID must currently
hold a sovereignty assessment producing `decision: 'autonomous'` from
`assessSovereignty()` in `meta/sovereignty.ts`. Trust score thresholds
and policy logic live in ChittyTrust; consult that service to understand
why a particular actor is or is not autonomous for this intent type.

## Amount cap configuration

The cap is set via the Worker secret/var
`MERCURY_AUTONOMOUS_AMOUNT_CAP_USD` (whole USD, string). Default is 500
USD if unset.

```bash
# Set to USD 250
npx wrangler secret put MERCURY_AUTONOMOUS_AMOUNT_CAP_USD
# (enter 250 at prompt)
```

Any single intent with `amount_cents > cap * 100` is refused with reason
`amount_cap_exceeded`. There is no batching path that bypasses this — to
move a larger amount autonomously, you would need to increase the cap
(and accept the corresponding risk), or split the obligation into
multiple intents each under the cap.

## How to manually approve a `requires_human` mercury_payment

When sovereignty produces `requires_human` for a mercury_payment intent:

1. The dispatcher writes a `sovereignty_refusal` row to `cc_actions_log`
(or the executor writes `payment_refusal` if dispatch's freshness
window was longer than 60s and let the snapshot through).
2. The intent status moves to `failed`. The current build does NOT
auto-create a parallel approval queue entry; manual approval requires:
a. Inspecting the refusal via the dashboard or
`SELECT ... FROM cc_actions_log WHERE intent_id = '<id>'`.
b. Performing the payment via the ActionAgent chat surface
(`execute_payment` tool), which routes through the same pure
runner but supplies an `autonomous` + fresh snapshot because the
human typing in chat IS the approval event. The chat path writes
its own `cc_actions_log` row without `intent_id`.
c. Then manually updating the failed intent's metadata to reference
the chat-executed `cc_actions_log.id` for audit linkage:
```sql
UPDATE cc_intents
SET metadata = COALESCE(metadata, '{}'::jsonb) || jsonb_build_object(
'manually_approved_via_chat', '<chat_actions_log_id>',
'approved_by', '<chitty_id>',
'approved_at', NOW()::text
)
WHERE id = '<intent_id>';
```

A dedicated dashboard approval flow is a follow-up (tracked in the
ADR-001 amendment notes).
Comment on lines +88 to +117

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Manual approval procedure is outdated and incorrect.

Lines 100-103 instruct operators to perform manual approval via the chat surface execute_payment tool, stating it "routes through the same pure runner but supplies an autonomous + fresh snapshot."

However, per the PR objectives and the test at tests/meta/executors/mercury-payment-failures.spec.ts:217-253, the chat surface was changed to refuse Mercury payments unconditionally (refusal reason: chat_surface_refuses_mercury) and directs users to the dashboard. The described chat-based approval flow no longer works.

This section must be rewritten to reflect the current implementation, which blocks Mercury payments from the chat surface entirely.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/runbooks/mercury-payment-executor.md` around lines 88 - 117, The
manual-approval section is outdated: the chat surface `execute_payment` path now
unconditionally refuses Mercury payments (`refusal reason:
chat_surface_refuses_mercury`, see
tests/meta/executors/mercury-payment-failures.spec.ts) so you must remove or
rewrite any instructions that tell operators to approve via the chat runner;
instead state that operators must use the dashboard or an out-of-band payment
mechanism, record the payment in `cc_actions_log` (since chat will not create an
approval row with `intent_id`), and then manually update the failed intent
metadata (the existing `UPDATE cc_intents ...` guidance and keys
`manually_approved_via_chat`, `approved_by`, `approved_at` should be revised to
reflect that the approval will reference the dashboard/out-of-band
`cc_actions_log.id` and not a chat action); also add a brief note referencing
`chat_surface_refuses_mercury` and the test file to justify the change.


## Rollback / cancellation

To refuse or cancel a queued `mercury_payment` intent BEFORE the daemon
dispatches it:

```sql
UPDATE cc_intents
SET status = 'failed',
metadata = COALESCE(metadata, '{}'::jsonb) || jsonb_build_object(
'cancelled_by', '<chitty_id>',
'cancelled_at', NOW()::text,
'cancel_reason', '<reason>'
)
WHERE id = '<intent_id>' AND status = 'pending';
```

`executeIntent` only acts on `status = 'pending'` intents (it atomically
claims via `UPDATE ... RETURNING`), so a status change to `failed`
prevents dispatch.

After the daemon has already called `executeIntent` and the Mercury API
returned success: the payment is in flight at Mercury. Cancellation must
go through Mercury's dashboard or API directly — the local
`cc_actions_log` row is audit-only and reversing it does not unwind a
real ACH transfer.

## Audit query

To inspect every payment-related action for a given intent:

```sql
SELECT
id,
intent_id,
attempt,
idempotency_key,
action_type,
target_type,
target_id,
description,
status,
error_message,
request_payload,
response_payload,
metadata,
executed_at
FROM cc_actions_log
WHERE action_type IN ('payment', 'payment_refusal')
AND intent_id = '<intent_id>'::uuid
ORDER BY executed_at ASC;
```

For a recipient-level audit:

```sql
SELECT id, intent_id, status, description, executed_at
FROM cc_actions_log
WHERE action_type IN ('payment', 'payment_refusal')
AND target_id = '<recipient_id>'
ORDER BY executed_at DESC
LIMIT 50;
```

For the chat surface (no `intent_id`):

```sql
SELECT id, status, description, metadata, executed_at
FROM cc_actions_log
WHERE action_type = 'payment'
AND intent_id IS NULL
ORDER BY executed_at DESC
LIMIT 50;
```

## Idempotency

The dispatcher computes a deterministic idempotency key per attempt:

```
sha256("{intent.id}:{attempt}:{intent_type}")
```

The partial unique index
`cc_actions_log (intent_id, idempotency_key) WHERE intent_id IS NOT NULL
AND idempotency_key IS NOT NULL` enforces single-row-per-attempt at the
database level. The dispatcher also short-circuits on any prior terminal
(`completed` or `failed`) row for the same `intent_id`, so a replay of a
finished intent returns the prior result without re-executing.

The Mercury API call passes `ctx.idempotencyKey` as Mercury's own
`idempotencyKey`, so Mercury de-dupes on the same value if the executor
is re-invoked between writing the audit row and Mercury responding.
Comment on lines +193 to +210

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Idempotency key formula is incorrect.

The formula at lines 197-199 includes {attempt}:

sha256("{intent.id}:{attempt}:{intent_type}")

However, per the PR objectives and the test comment at tests/meta/executor-pr106-criticals.spec.ts:101-102, the idempotency key was refactored to exclude the attempt number and is deterministic on intent.id + intent_type only:

sha256("{intent.id}:{intent_type}")

The runbook must be corrected to remove {attempt} from the formula to match the implemented contract.

📝 Proposed fix
-sha256("{intent.id}:{attempt}:{intent_type}")
+sha256("{intent.id}:{intent_type}")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## Idempotency
The dispatcher computes a deterministic idempotency key per attempt:
```
sha256("{intent.id}:{attempt}:{intent_type}")
```
The partial unique index
`cc_actions_log (intent_id, idempotency_key) WHERE intent_id IS NOT NULL
AND idempotency_key IS NOT NULL` enforces single-row-per-attempt at the
database level. The dispatcher also short-circuits on any prior terminal
(`completed` or `failed`) row for the same `intent_id`, so a replay of a
finished intent returns the prior result without re-executing.
The Mercury API call passes `ctx.idempotencyKey` as Mercury's own
`idempotencyKey`, so Mercury de-dupes on the same value if the executor
is re-invoked between writing the audit row and Mercury responding.
## Idempotency
The dispatcher computes a deterministic idempotency key per attempt:
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 197-197: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/runbooks/mercury-payment-executor.md` around lines 193 - 210, Update the
idempotency key formula in the runbook to match the implementation and tests:
change the documented SHA-256 input from
sha256("{intent.id}:{attempt}:{intent_type}") to
sha256("{intent.id}:{intent_type}"), removing the "{attempt}" segment so the
idempotency key is deterministic on intent.id and intent_type only and aligns
with the executor contract and the test expectations referenced by the executor
PR.


## Related files

- `meta/executors/mercury-payment.ts` — executor + pure runner
- `meta/executors/dispatch.ts` — dispatcher (sovereignty re-reckon, audit write)
- `meta/sovereignty.ts` — sovereignty gate (calls trust.chitty.cc)
- `src/agents/tools/actions.ts` — chat surface (`execute_payment` tool)
- `src/lib/integrations.ts` — Mercury API client (`mercuryClient`)
- `tests/meta/executors/mercury-payment.spec.ts` — refusal-path integration tests
Loading
Loading