Safety contract

Modes, tenant rules, refusals, confirm hashes, typed tokens, production rules and readback, as metro enforces them.

View .md

metro can change invoices, credits, contracts and customer access. This page is the contract between metro, your agent and you. Most of it is enforced in code. The rest is behavior every agent must follow.

The rules

  1. Read-only by default. After metro init, metro can look, preview and validate. Writes need metro mode write, and only the human decides to raise it.
  2. Sandbox first. A change is proven on sandbox before production is discussed.
  3. Always name the tenant. Pass --sandbox or --prod. metro never picks production on its own.
  4. Preview, confirm, apply, read back. Every write follows the same ladder.
  5. Production needs the human's explicit typed yes in chat for that specific change. A yes for one change never covers the next. A green preview is not authorization, and neither is a key being present.
  6. Never print keys. Don't cat or echo ~/.metro/.env. metro shows fingerprints like sha256:80f0714120b8, never values.

Modes

The mode is a ceiling, saved in ~/.metro/mode.json.

ModeLive readsLive writesSet with
local-onlyblockedblockedmetro mode set local-only
read-only (default)allowedblockedmetro mode set read-only or metro mode reset
read-writeallowedallowed, with confirmation tokensmetro mode write or metro mode set read-write

metro mode shows the current mode. A dry run never needs write mode. An --apply in read-only mode is refused with MTR-MODE-WRITE-BLOCKED and the next command to run.

The ladder for every write

Preflight. Reads only. Writes a plan file with the exact request bodies and prints a payloadHash: the sha256: of the canonical plan, which covers the tenant and the key fingerprint.

metro contract expire preflight --ingest-alias=acme-corp --cutover-mode=next-hour --sandbox --out=expire.json

Review. The agent shows you the plain-English change and the hash, then waits for your yes.

Dry run. apply with --confirm-hash and without --apply prints the requests and writes nothing.

metro contract expire apply --file expire.json --sandbox --confirm-hash sha256:...

Apply. Add --apply and the typed token for the tenant.

metro contract expire apply --file expire.json --sandbox --confirm-hash sha256:... \
  --apply --confirm-live-apply=I_UNDERSTAND_THIS_WILL_WRITE_LIVE_METRONOME_SANDBOX

Read back. metro reads each object back and prints expected vs actual. Any mismatch exits 3. Every apply writes a log to ~/.metro/logs/<op>/, or to METRO_LOG_DIR.

Typed tokens

WhereFlag
Sandbox write--confirm-live-apply=I_UNDERSTAND_THIS_WILL_WRITE_LIVE_METRONOME_SANDBOX
Production write--prod --confirm-prod-apply=I_UNDERSTAND_BILLING_SIDE_EFFECTS
Guarded read (metro api read)--confirm-live-read I_UNDERSTAND_THIS_WILL_READ_LIVE_METRONOME
Guarded sandbox write (metro api apply)--confirm-live-apply I_UNDERSTAND_THIS_WILL_WRITE_LIVE_METRONOME_SANDBOX, plus an apply gate and an approval record

The guarded metro api apply chain is sandbox-only. Production apply through it is blocked.

Refusals

A refusal exits 2. Stop, quote it to the human, and don't work around it.

CodeWhen
MTR-TENANT-001No --sandbox or --prod. metro never infers the tenant.
MTR-TENANT-003Both flags passed, or apply's tenant differs from the plan's. A sandbox plan can never be applied to production.
MTR-TENANT-004The key fingerprint changed between preflight and apply.
MTR-TENANT-KEY-001The tenant's key isn't set.
MTR-TENANT-KEY-002--prod, and the production key equals the sandbox key.
MTR-TENANT-KEY-003--sandbox, and the sandbox key equals the production key.
MTR-PLAN-001The file isn't a plan for this command.
MTR-PLAN-002The plan was edited after preflight.
MTR-HASH-001, MTR-HASH-002--confirm-hash is missing, or doesn't match the plan.
MTR-MODE-WRITE-BLOCKED--apply while the mode is read-only or local-only.
MTR-LIVE-APPLY-001A sandbox apply without the sandbox token.
MTR-PROD-APPLY-BLOCKEDA production apply without the production token.

Two non-refusal stops matter too. MTR-PRECHECK (exit 1) means live state changed since preflight; run preflight again. MTR-APPLY-STEP-FAILED (exit 1) names the step that failed and the steps that completed before it.

Production rules

Before any production write, write down:

  1. The customer's identifiers: Metronome customer ID, contract ID, billing-provider customer ID, and any external ID you matched on.
  2. The tenant, and which key metro will use.
  3. The current state, from read-only evidence.
  4. The desired state, in business terms.
  5. The exact endpoints and payload shapes.
  6. The expected invoices, balance changes, billing-provider behavior and access effects.
  7. The dry-run result, and where its file was written.
  8. A canary: one customer or one object first.
  9. The readback you'll run.
  10. Rollback or remediation.

Readback

The samples below are illustrative, in the exact format metro prints. Readback is the proof. A 200 from Metronome is not: some contract edits return 200 and silently don't apply.

Applied (sandbox).
Read-back (expected vs actual):
  ok   contract.id: expected "00000000-0000-4000-8000-000000000002" actual "00000000-0000-4000-8000-000000000002"
  ok   contract.ending_before: expected "2026-12-01T19:00:00.000Z" actual "2026-12-01T19:00:00.000Z"
Log: ~/.metro/logs/contract-expire/apply-2026-11-03T19-21-34-560Z.json

When it fails, metro says so and exits 3:

Applied, but read-back FAILED (sandbox).
Read-back (expected vs actual):
  FAIL quantity at 2027-02-01T00:00:00.000Z: expected 15 actual 12

Don't re-run the same plan. Use the log and a fresh read, for example metro ops who <customer-id> --sandbox, to find where expected and actual diverge. Fix the cause, then preflight again.

Retries

Writes are retried only on HTTP 429. A timed-out create is never sent twice. Every write metro builds carries a uniqueness_key where Metronome accepts one, so a repeat returns 409 instead of a duplicate. Customer and rate-card creates have no uniqueness key, so metro uses the ingest aliases and the card name as the anchor and checks them again just before writing.

On this page