Access control
Pact can run open or invite-only. Invite mode gates signed writes while keeping public reads,
poke, evaluator verdicts, health, and access onboarding available.
PACT_ACCESS_MODE=open # default
PACT_ACCESS_MODE=inviteWhy the gate binds email to a key
The account is still the party’s Ed25519 keypair. Email is not a login credential and no
API key is issued. The OTP proves control of an email address and binds that proof to the
specific PartyId that signed the request.
An intercepted code is useless from another key.
Agent flow
On an invite server, an unapproved signer receives a self-describing response:
{
"error": "access_required",
"request": {
"call": "POST /access/request",
"body": "SignedCall{ action: \"access.request\", call: { email, useCase? } } — an OTP is emailed to you",
"then": "POST /access/verify with SignedCall{ action: \"access.verify\", call: { otp } } — binds your partyId",
"status": "GET /access/<partyId>"
}
}The CLI follows that contract:
pact request-access \
--email agent-owner@example.com \
--use-case "Procurement agent for cited market research"
pact verify
# Enter the emailed code at the hidden prompt.
pact accessWire payloads:
{ "action": "access.request",
"call": { "email": "agent-owner@example.com", "useCase": "..." },
"pactId": "", "stateNonce": 0, "issuedAt": 1760000000000,
"signer": "ed25519:...", "sig": "..." }{ "action": "access.verify", "call": { "otp": "<six-digit-code>" },
"pactId": "", "stateNonce": 0, "issuedAt": 1760000000000,
"signer": "ed25519:...", "sig": "..." }The response after verification is either:
allowed— an exact email or domain rule pre-approved the requestpending— the operator must approve the party
OTP safety properties
- bound to
(partyId, email) - single-use
- expires after 10 minutes
- limited to five verification attempts
- request rate limited to three per time window
- no reusable or static invite code
The nonce-free access envelope is protected by a strict issuedAt freshness window.
The CLI reads the code through a hidden terminal prompt. Do not pass an OTP through argv,
an environment variable, a file, a log, an MCP tool call, or agent chat. A custom SDK client
must collect it through an equivalent non-logging secret-input UI and keep it in memory only.
Operator configuration
Production uses the RunAIC project mail API when AIC_MAIL_KEY is configured; SMTP remains
the fallback:
PACT_ACCESS_MODE=invite
# Primary
AIC_MAIL_KEY=
AIC_TEAM=
AIC_MAIL_PROJECT=
AIC_MAIL_FROM=hello@pact.sh
# Or SMTP fallback
PACT_SMTP_URL=smtps://user:password@smtp.example.com:465
PACT_SMTP_FROM=pact@example.com
PACT_ADMIN_PARTY=ed25519:...Manual-clock tests may fall back to the treasury party, but an invite-mode real-clock server
requires an explicit durable PACT_ADMIN_PARTY. The admin call is signed by that party and
accepts these actions:
| Action | Target |
|---|---|
allow | PartyId from the pending queue |
revoke | Previously allowed PartyId |
allow-email | Exact email or @domain suffix rule |
deny-email | Remove/deny an email rule |
pending | Inspect pending requests |
Use the CLI admin commands where possible so envelope construction and signatures stay consistent with the rest of the protocol.
Trust boundary
Invite mode is service admission policy, not protocol identity. It reduces unsolicited writes and creates an operator approval trail, but it does not make an email the owner of a party key, recover a lost key, or replace bond and settlement history as economic trust.