The problem

Automation that can do something irreversible needs a way to ask a person who is usually not at a keyboard. A production deploy at 02:00. A database restore during an incident. A refund batch. A migration an agent just proposed.

Two things go wrong in practice. The change waits hours for somebody to open a laptop. Or somebody taps approve on a phone screen without reading what they approved.

A phone call fixes the first problem. It reaches a person in about thirty seconds with nothing to install. It does not fix the second one by itself, which is why this gate never accepts a bare yes.

What it does

Phone Approval Gate puts exactly one CALL-E call in the path of the risky step.

  • It rings the enrolled owner, discloses that the caller is not a person, then reads the change out loud: title, one line of detail, who requested it, which environment.
  • To approve, the person has to read back a six digit code that was printed on the request channel: the CI log, the agent reply, the change ticket. CALL-E never speaks that code. Reading it back is what shows the person could see the change they are approving.
  • It walks a ladder, one call per approver in order, stopping at the first decision. A rejection anywhere is final.
  • Single control or dual control. Dual needs two approvals from two different handsets, each with its own code, which is the two person rule a change board already runs on paper.
  • Every attempt lands in an append only record: who was called, what was said, which window it fell in, what the verdict was. The record is checked before the verdict is trusted, so a rewritten line is refused rather than believed.

Why the code matters

An approval is only worth something if the approver saw what they approved. Reading a code back over the phone binds the two together, because the code lives on the change and nowhere else. A person who cannot see the change cannot produce the code. A person who says yes without it gets secret_not_returned and the deploy is held.

That is the same shape as a verifier impersonation check in NIST SP 800-63B: the thing the person returns has to have come from the channel the request was raised on.

What it refuses to do

  • It will not approve on a call it could not read. A call CALL-E has not finished with leaves call_state_unknown and the ladder stops rather than guessing.
  • It will not accept a decision from outside the window it was asked in. The window is checked against the local clock and against CALL-E's own completed_at. A completion time nobody can parse is completion_time_unknown rather than a pass.
  • It will not carry a credential to a host nobody vetted. The base URL is checked against an allowlist before the SDK client ever holds the key.
  • It will not place a second call for a retry. The idempotency key carries a digest of the exact payload, so a replay is the same call rather than a second handset ringing.

How it was built

TypeScript on the @call-e/calle SDK, tsx plus node:test, no build step. A local fake CALL-E server stands in for the API, so the whole thing runs and is testable with no account and no credits. The same code path talks to the real API when a key is present, which is what makes the fake honest rather than a mock of a mock.

There is a companion agent skill, skills/deployment-approval-call, so an agent in Claude Code or Codex can raise the gate itself instead of a human wiring the call.

What is real

123 tests pass, tsc --noEmit is clean, npm run demo runs four cases end to end against the fake and writes four records. The demo video was recorded from that real output. Every call scene in it is labelled as the local fake, because a real call rings a real handset and costs real money.

Built With

  • agent-skills
  • call-e
  • call-e-sdk
  • ci-cd
  • github-actions
  • json-schema
  • nist-sp-800-63b
  • node-test-runner
  • node.js
  • typescript
Share this project:

Updates

Submission history