Skip to content

GA /v1 readiness: settle API contract, regenerate SDKs, slim CLI, re-curate MCP (hosted-only)#247

Merged
jiashuoz merged 14 commits into
mainfrom
ga-api-review-fixes
Jun 20, 2026
Merged

GA /v1 readiness: settle API contract, regenerate SDKs, slim CLI, re-curate MCP (hosted-only)#247
jiashuoz merged 14 commits into
mainfrom
ga-api-review-fixes

Conversation

@jiashuoz

Copy link
Copy Markdown
Member

Summary

Pre-GA readiness pass over the /v1 API, both SDKs, the CLI, and the MCP server — the goal was to freeze a clean, consistent, forward-compatible public contract. Walked each surface tool-by-tool/endpoint-by-endpoint, applied the agreed fixes, regenerated the SDK bases, and re-curated the MCP tool surface to match.

Decisions are logged in docs/design/ga-api-review.md.

API (/v1)

  • Slice A/B/C/D: settled the contract — read_status (was status), email path/param naming (was address), unified SendResultView for send+approve (MSG-9; message_id is the e2a msg_ id), AccountView shape + agent-scoped agent_address, real keyset pagination for conversations + suppressions, ?confirm=DELETE on destructive ops, enum tightening on agent/webhook fields, struct-tag audit (nullable:"false" for always-present arrays), event renames (email.approval_accepted/approval_rejected).
  • make spec regenerated api/openapi.yaml; TestSpecGoldenNoDrift + spec-check green.

SDKs (TS + Python)

  • Regenerated both bases from the reshaped spec via OpenAPI Generator; re-homed the hand-written ergonomic layer (renamed types, cursor-following lists, email params, listen(email) required).
  • Phase-2 hardening: ERR-1 (400→validation), WH-SIG-1 (missing-header guard), WS-1/2 (URL-encode key + typed fatal-handshake stop).
  • Python messages.list: public kwarg from_ (PEP-8), translated to the generated var_from internally so the mangled name never leaks.
  • generate-sdk-check green (generated trees byte-identical).

CLI

  • Slimmed to the non-duplicative surface: login, listen (with --forward), config. Agent/message/domain/webhook CRUD lives in the MCP tools, SDKs, and dashboard.

MCP (hosted-only)

  • Re-curated to the reshaped /v1 (§6a): renames (send_message, get_attachment, approve_message, reject_message), create_agent takes email (dropped slug/agent_mode/webhook_url), update_agent adds hitl_mode/inbound_policy/inbound_allowlist, whoamiAccountView, agent_emailemail everywhere, read_status filter.
  • Stdio transport removed — hosted-only at https://api.e2a.dev/mcp (Streamable-HTTP, OAuth-first; Bearer API key also accepted). Removed the stdio entry/test/publish workflow; examples are hosted-only. E2A_AGENT_EMAIL removed.
  • Independent-review fixes: restored list_webhook_deliveries (the events log is not the delivery ledger — fixes a dangling test_webhook reference); enumerated all 12 webhook events; added get_agent; cleaned stale comments.
  • 35 tools, build clean.

Verification

  • Go: spec-check ✓, generate-sdk-check ✓, make test-unit
  • TS SDK ✓ · CLI ✓ · MCP (116 tests) ✓ · Python (131) ✓

Deploy note

api.e2a.dev/mcp requires the api.e2a.dev ingress to path-route /mcp to the MCP process (infra config, not in this repo).

Follow-ups (not in this PR)

  • Web dashboard consumer-port to the new contract (CI-green via mocks, runtime-stale).
  • MCP tool annotations (readOnlyHint/destructiveHint/idempotentHint) — §6a fast-follow.

🤖 Generated with Claude Code

jiashuoz and others added 13 commits June 19, 2026 20:26
Pre-GA API review decisions (docs/design/ga-api-review.md), Phase-1 contract
changes that don't need a store change or the path rename:

account/info
- I-1 GET /v1/info returns `version` (single-sourced APIVersion const)
- A-1/A-2 GET /v1/account → AccountView with identity (user, scope,
  agent_address for agent-scoped only); whoami now works for both scopes

domains
- D-4 registerDomain.domain required; D-1 sending_status enum;
  D-5 deleteDomain ?confirm=DELETE

agents
- AG-5 createAgent returns full AgentView; AG-1/2 drop slug, email required,
  shared-domain detected by email domain; AG-6 deleteAgent ?confirm=DELETE
  (checked after ownership); AG-7 update-agent input enums

messages / outbound / hitl
- MSG-1 status→read_status (field + filter); MSG-3 to/subject/body required;
  MSG-9 unified SendResultView (message_id is the e2a id, +provider_message_id
  /sent_as/method enum/edited; fixes send returning the SES id); MSG-10
  RejectRequest; MSG-11 AuthVerdict; MSG-12 auth_headers optional

webhooks
- WH-1 url/events required; WH-2 events item-enum; WH-3 signing_secret only in
  CreateWebhookResponse (removed from WebhookView); WH-6 *Request/*Response
  naming (RotateSecretResponse, TestWebhookResponse, RedeliverEventRequest)

A-3 nil→[] sweep (orEmpty + nullable:"false") across all response views +
top-level UserExport; nested export rows kept raw (F7).

Slice B event rename: email.approved→email.approval_accepted,
email.rejected→email.approval_rejected (consts + 6 enum sites + e2e + docs).

Tests, contract scenarios, and e2e updated in lockstep. make spec regenerated;
spec-check + ./internal/... (-p 1) green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Slice C — path param {address} → {email}
- All per-agent /v1 routes + the chi WebSocket route renamed (route placeholder
  + path: tag + chi.URLParam). Go field name Address kept (internal).
- Suppression recipient /v1/account/suppressions/{address} stays {address}
  (it's a recipient address, not the agent identifier).
- spec_test path expectations updated; test URLs use real emails so unaffected.

Slice D — real cursor pagination (+ C-1)
- suppressions (A-5): keyset on (created_at, address). Store ListSuppressions
  gains limit + after-key; dep sig + handler updated (fetch limit+1, encode
  next_cursor). Auto-grows on every bounce/complaint, so it needed real paging.
- conversations (CV-3): keyset on (last_message_at, conversation_id) via
  ConversationListFilter.After* + a HAVING keyset predicate; handler fetches
  limit+1 and encodes a filter-identity cursor.
- C-1: ConversationSummaryView timestamps string → time.Time + format date-time
  (consistent with every other timestamp; fixes untyped-string SDK gen).

Verified: go build, make spec-check, ./internal/httpapi (-p 1), integration
compile, real-Postgres identity+delivery store tests, and new handler cursor
round-trip tests (TestListSuppressionsPagination / ...BadCursor).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ption)

Audit of the implementation against docs/design/ga-api-review.md (48/49 spec
assertions passed on first check):

- WH-7: bump listWebhookDeliveries limit to default 100 / max 500 (the doc said
  "single page + large default limit"; the impl still had default 50 / max 100).
- Doc reconcile: the lone remaining `Result` schema is `Message.auth` on the
  raw identity.Message export row (UserExport.messages). identity can't import
  the httpapi AuthVerdict view (import cycle), so MSG-11 applies to the public
  message-read views (done) and the export row stays raw — same class as the
  A-3 nested-export exception. Named explicitly in the F7 exception note.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nomic layer

Slice E (SDK regen). Regenerated both generated/ bases from the updated
api/openapi.yaml (OpenAPI Generator v7.16.0, deterministic) and re-homed the
hand-written ergonomic layer onto the renamed/reshaped types:

- Type renames: LimitsView→AccountView, ApproveResultView→SendResultView (approve
  now returns the unified result), CreateAgentResponse→AgentView,
  RejectInputBody→RejectRequest, RotateSecretBody→RotateSecretResponse,
  TestWebhookOutputBody→TestWebhookResponse, RedeliverEventInputBody→
  RedeliverEventRequest; webhooks.create→CreateWebhookResponse.
- read_status: list filter + ListMessagesParams (TS) / list() kwarg (Python)
  renamed status→read_status to match the API.
- Real pagination: conversations.list + suppressions.list now follow next_cursor
  (CV-3/A-5) instead of single-page.
- Deletes pass ?confirm=DELETE (the typed .delete() is the confirmation; AG-6/D-5);
  account.delete() drops its confirm arg.
- Python enum validators stripped on regen (16 models) — forward-compat preserved.

TS 84 tests + Python 131 tests green. generate-sdk-check is clean once committed
(deterministic regen). MCP + CLI do NOT yet compile against the reshaped SDK —
that's the separately-tracked consumer-port / §6a MCP tool re-curation round.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…es email

Hand-written ergonomic layer (both SDKs), after reviewing the generated layers
(found strong: TS camelCase/Date/ApiException<ErrorEnvelope>; Python
snake_case/var_from/datetime/plain-str enums):

- HL-1: rename the agent identifier param address→email across every per-agent
  method (agents/messages/conversations + listen) in both SDKs — consistent with
  AG-3 and the generated layer (getAgent(email)).
- HL-2: drop the E2A_AGENT_EMAIL listen() env fallback; listen(email) now
  requires an explicit email (design §9a removed the default-agent env). Tests
  updated (TS client/ws, Python).

Accepted as-is: Python async-only (HL-3); generated TS string enums (TS-1,
forward-compat safe via ObjectSerializer passthrough). Deferred: inbound
.parse()/.reply() + typed event payloads (F5/S2).

TS 84 tests + Python 131 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…1, WS-1/2)

Class-by-class review of the hand-written SDK layers surfaced three real gaps,
fixed in both SDKs (or TS-only where Python already had the fix):

- ERR-1 (both): the typed-error status bucket lacked 400, so 11 of the API's 16
  distinct 400 codes (confirmation_required, too_many_recipients, invalid_domain,
  invalid_slug, reserved_domain, webhook_cap_reached, …) degraded to the bare
  E2AError. Map 400 → E2AValidationError.
- WH-SIG-1 (both): verify_webhook_signature threw a raw TypeError/AttributeError
  on a missing/non-string X-E2A-Signature header instead of returning false
  ("never throws" contract). Guard it.
- WS-1 + WS-2 (TS; Python already had both): TS WebSocket injected the raw API
  key into ?token= (now encodeURIComponent) and reconnected forever on a fatal
  4xx handshake. TS now detects the 4xx (unexpected-response), surfaces a typed
  E2AAuthError/E2APermissionError, and stops reconnecting — F6 parity with
  Python's _fatal_error_for_status.

Reviewed clean (no change): pagination (AutoPager dual loop-guards), retry
(unsafe-write gating incl. account-delete exclusion). TS 84 + Python 131 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…cate CRUD)

The CLI isn't a core surface for an agent-first gateway: agents use MCP,
programmatic callers use the SDK, operators use the dashboard. The ~12
CRUD/messaging commands duplicated those and broke on every contract change.

Confirmed the WebSocket is noMcp (MCP agents poll list_messages / use webhooks;
the real-time WS path for local agents is the SDK's client.listen()), so the
CLI's only non-duplicative capability is the local dev forward-proxy.

Kept: login, listen (with --forward, the `stripe listen --forward-to` pattern),
config. Dropped: agents, conversations, domains, events, forward, inbox, labels,
pending, read, reply, send, webhooks (+ their tests). Rewrote the dispatcher +
README. ~4316 → ~1804 LOC; builds clean, 54 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e reshaped /v1

web/public/openapi.yaml is a build-time copy of api/openapi.yaml (web's
sync-openapi script, run on prebuild); refresh the committed copy so the
dashboard API-reference page reflects the GA /v1 contract. Pure copy —
byte-identical to api/openapi.yaml.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The MCP server (hosted Streamable-HTTP at /mcp + stdio self-host) now matches
the GA /v1 contract. Wrapper (client.ts) + tools + tests updated; SDK + CLI
unaffected (all green).

Tool surface:
- Renames: send_email→send_message, get_attachment_data→get_attachment,
  approve_pending_message→approve_message, reject_pending_message→reject_message.
- Added: get_domain (poll sending_status).
- Removed: list_webhook_deliveries (folded into the events log).
- create_agent: drop slug/agent_mode/webhook_url → { email, name? }, returns AgentView.
- update_agent: drop agent_mode/webhook_url; add hitl_mode/inbound_policy/
  inbound_allowlist.
- whoami: returns the account identity (AccountView: user/scope/agent_address),
  no more default-agent auto-resolution.
- Every per-agent tool arg agent_email→email; list_messages status→read_status;
  get_message response status→read_status; webhook event names →
  email.approval_accepted / email.approval_rejected.

Deviation from §6a (noted): kept list_pending_messages + get_pending_message —
the API has no pending filter on list_messages (read_status is unread/read/all),
so these client-side hitl_status scans are the only way to view the approval
queue. Renamed approve/reject to match the API ops.

E2A_AGENT_EMAIL removed everywhere (config + wrapper + tool docs): an
agent-scoped credential resolves its agent server-side; account callers pass
`email`. Also fixed SDK ListMessagesParams.read_status→readStatus (camelCase
consistency; Python stays read_status).

MCP build clean; 117 MCP tests green. SDK 84, CLI 54, Python 131 — all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…2a.dev/mcp

Most users connect via the hosted MCP, so drop the stdio transport entirely and
make the hosted Streamable-HTTP server the single surface.

Remove stdio:
- Delete mcp/src/index.ts (stdio entry) + mcp/tests/events-stdio.test.ts +
  .github/workflows/publish-mcp.yml (the stdio npm publish). The hosted server
  runs via Docker (publish-mcp-http.yml → node dist/bin/http.js).
- package.json: drop the `e2a-mcp` bin; main → ./dist/bin/http.js.
- server.json: drop the npm/stdio `packages` block (+ the removed
  E2A_AGENT_EMAIL env); keep the streamable-http `remotes` entry (OAuth-first,
  Bearer API key also accepted).
- Examples: remove every stdio variant; the hosted agent.py is now the single
  example per framework (adk/crewai/langchain/openai-agents); codex config keeps
  only the hosted block. Full doc sweep of mcp/README + examples (no npx/stdio/
  E2A_AGENT_EMAIL left).

Hosted endpoint: migrate mcp.e2a.dev → api.e2a.dev/mcp (§6a: api.<host>/mcp,
path-routed) across src (allowedHosts default), server.json, plugin.json, tests,
examples. (Deploy note: the api.e2a.dev ingress must path-route /mcp to the MCP
process.) Design doc left as-is (it intentionally contrasts both hosts).

MCP build clean; 114 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dpoint

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…bhook_deliveries

Independent multi-agent review of the MCP tools vs the SDK + API surfaced a
BLOCKER, two MAJORs, and stale comments. Fixes + two new tools (35 total).

New tools:
- get_agent (GET /v1/agents/{email} → AgentView): fetch one agent's full config
  (HITL/inbound/sending) without listing all agents — fills a real read gap.
- list_webhook_deliveries (GET /v1/webhooks/{id}/deliveries): RESTORED. The §6a
  "folds into the events log" rationale was wrong — the events log is the event
  stream, not the per-webhook delivery ledger (status/attempts/last_error). This
  also fixes the BLOCKER: test_webhook's description pointed at a non-existent
  `list_events {webhook_id}` filter.

Fixes:
- create_webhook + list_events: enumerate all 12 valid event types (were
  documenting only 5; agents couldn't discover email.delivered/bounced/
  complained/flagged, domain.sending_verified/sending_failed/suppression_added).
- Python SDK messages.list: public kwarg var_from→from_ (PEP-8); translate to the
  generated base's var_from internally so the generator's mangled name never
  leaks into the public surface. (TS stays `from`; idiomatic per language.)
- Stale code comments: attachments.ts (send_email/approve_pending_message →
  send_message/approve_message), http-server.ts resolution-order (dropped the
  removed E2A_AGENT_EMAIL step 1).

MCP build clean; 116 tests green (+behavior tests for both new tools). SDK/CLI/
Python all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…auth from 422)

The create_agent/send auth-rejection steps sent body:{} and expected 401.
With email now required on create_agent (and to/subject/body on send), an
empty body trips Huma schema validation (422) before the per-handler
requirePrincipal auth check (401) runs — so the step saw 422, not 401.

Hold the request otherwise-valid so the auth-rejection steps test auth, not
body validation (standard practice: bad-auth tests send a well-formed request).
Pre-existing 422-before-401 ordering for body endpoints is unchanged; this only
fixes the now-stale scenario bodies.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jiashuoz
jiashuoz merged commit 5096007 into main Jun 20, 2026
13 checks passed
@jiashuoz
jiashuoz deleted the ga-api-review-fixes branch June 20, 2026 18:35
jiashuoz added a commit that referenced this pull request Jun 20, 2026
§6a was written as a target; the GA build (PR #247) ratified several deviations.
Add an authoritative as-built banner at the top of §6a and fix the two inline
claims that were factually false as standalone statements:

- arg/path is email/{email}, not address/{address} (AG-3)
- E2A_AGENT_EMAIL / E2A_AGENT_ADDRESS removed (no default-agent env)
- stdio transport removed — hosted-only at api.e2a.dev/mcp (OAuth + Bearer key)
- 35 tools, not 31: kept list_pending_messages/get_pending_message (no
  pending_approval filter) + list_webhook_deliveries (events log != delivery
  ledger, so update #6 is rejected); added get_agent
- approve/reject are two routes; get_attachment re-fetches the full message
- recommended updates: #8 done, #6 rejected; #1/#2/#4/#5/#7 pending, #3 partial
- MCP_ALLOWED_HOSTS/MCP_PUBLIC_URL retargeted to api.e2a.dev (done)

Doc-only; the drift-gate tool→operationId map remains authoritative for the
live surface.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jiashuoz added a commit that referenced this pull request Jun 20, 2026
…curacy

Independent + adversarial review (both READ-ONLY) of #253:
- Adversarial: SAFE — proved (in-process + real HTTP) a hidden admin tool can't
  be invoked (wrapper never runs), fail-closed scope across 18 inputs, no
  client-controllable scope, session bearer-binding holds. No security findings.
- Independent: pass-with-risks. Both converged on the tiers.ts comment
  referencing a non-existent `assertToolTiersComplete`.

Fixes (regression test per finding):
- Implement the real guard `assertToolTiersComplete(registered)` in tiers.ts and
  add a test that collects the TRUE (un-gated) registered tool names via a
  name-recording fake server and asserts the tier map covers them exactly. This
  closes a real gap both reviews under-analyzed: an untiered new tool is gated
  out of EVERY scope and would otherwise vanish silently (the exact-35 list test
  can't catch it — gating hides it before listing). Fixed the misleading comment.
- Add a fail-closed regression test: toolNamesForScope(unrecognized) → runtime.
- README: corrected the create_agent row (full email, no slug/mode) and replaced
  the stale `agent_mode: "cloud"` signature note with the top-level-webhooks
  reality (the #247-era drift my create_agent edit would otherwise contradict).

Nits accepted as-is: registerTool monkey-patch uses an unknown[] cast (loses
overload types; functionally correct — args forwarded verbatim).

123 MCP tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jiashuoz added a commit that referenced this pull request Jun 20, 2026
… HITL approve/reject (#253)

* feat(mcp): scope-gate the tool surface by credential tier (§6a #1)

A deployed agent no longer carries all 35 tools or holds delete-domain power.
The MCP surface is now filtered to the connecting credential's scope:
  - agent  → the 16 runtime/inbox tools
  - account → runtime + admin (all 35)

- tools/tiers.ts (new): the single-source-of-truth tool→tier map (RUNTIME_TOOLS
  / ADMIN_TOOLS + toolNamesForScope), mirroring §6a's "drift-gate records each
  tool's tier".
- server.ts: buildServer gates registration at one seam (intercepts
  registerTool, skips tools outside the scope's allowed set). register*Tools
  stay scope-agnostic. Hidden = unlisted AND uncallable.
- client.ts: McpClient gains a `scope` field (default "account"; set per session).
- http-server.ts: buildSessionClient resolves scope + the bound agent from
  whoami (GET /account) instead of the listAgents single-agent guess. agent
  scope pins whoami.agentAddress; account scope has no default (explicit email
  per §6a — the single-agent auto-resolve is dropped). whoami non-auth failure
  fails closed to the runtime tier; 401 → InvalidBearerError.

Gating is a decision-space/UX optimization, NOT the security boundary — the
backend enforces scope per-handler regardless (CRITICAL-1 fix), so a mis-listed
tool is never load-bearing.

Tests: account→35, agent→16 (admin hidden), hidden admin tool errors + its
wrapper never runs; http session-init rewritten for whoami-based scope/agent
resolution (agent pins bound agent; account no default; non-auth fail-closed;
401 rejects). 119 MCP tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(mcp): add tool annotations to every tool (§6a #2)

readOnlyHint / destructiveHint / idempotentHint on all 35 tools, so clients can
auto-approve reads, flag the destructive deletes, and de-risk retries — and a
prerequisite for the Connectors-directory listing.

Classification:
- readOnlyHint: true — all 17 list_*/get_*/whoami reads.
- destructiveHint: true + idempotentHint: true — the 3 CASCADE deletes
  (delete_agent/domain/webhook).
- idempotentHint: true, destructiveHint: false — the 5 idempotent updates
  (update_agent/webhook, update_message_labels, verify_domain, register_domain).
- destructiveHint: false — the 10 non-destructive writes (create/send/reply/
  forward/approve/reject, create_webhook, rotate_secret, test_webhook,
  redeliver_event).

Test: lists tools (account scope) and asserts every tool carries annotations
with the right hints per category. 120 MCP tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(mcp): reflect scope-gating + annotations (§6a #1/#2 done)

- design §6a as-built banner: #1 scope/tier-gating and #2 annotations flipped
  from PENDING → done (with where: tiers.ts + server.ts seam, whoami signal;
  note that gating is UX, the backend ceiling per PR #251 is the boundary).
- mcp/README: the visible tool set is now scope-dependent (agent ~16 / account
  35), every tool carries annotations; fixed the whoami description (account
  identity, not "agent's full record").

Follow-up flagged (pre-existing #247-era README drift, not this change): the
README still references removed `slug`/`agent_mode`/cloud-mode in create_agent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(mcp): address review findings — real tier-drift guard + README accuracy

Independent + adversarial review (both READ-ONLY) of #253:
- Adversarial: SAFE — proved (in-process + real HTTP) a hidden admin tool can't
  be invoked (wrapper never runs), fail-closed scope across 18 inputs, no
  client-controllable scope, session bearer-binding holds. No security findings.
- Independent: pass-with-risks. Both converged on the tiers.ts comment
  referencing a non-existent `assertToolTiersComplete`.

Fixes (regression test per finding):
- Implement the real guard `assertToolTiersComplete(registered)` in tiers.ts and
  add a test that collects the TRUE (un-gated) registered tool names via a
  name-recording fake server and asserts the tier map covers them exactly. This
  closes a real gap both reviews under-analyzed: an untiered new tool is gated
  out of EVERY scope and would otherwise vanish silently (the exact-35 list test
  can't catch it — gating hides it before listing). Fixed the misleading comment.
- Add a fail-closed regression test: toolNamesForScope(unrecognized) → runtime.
- README: corrected the create_agent row (full email, no slug/mode) and replaced
  the stale `agent_mode: "cloud"` signature note with the top-level-webhooks
  reality (the #247-era drift my create_agent edit would otherwise contradict).

Nits accepted as-is: registerTool monkey-patch uses an unknown[] cast (loses
overload types; functionally correct — args forwarded verbatim).

123 MCP tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(mcp): move approve/reject to admin tier — agents must not self-approve HITL

§6a listed approve_message/reject_message in the runtime (agent) tier, but an
agent-scoped credential can only ever approve its OWN held outbound — i.e.
self-approval, which defeats the human-in-the-loop gate. Approval is an
account-owner / human action (or the magic-link browser flow), so these tools
are now admin (account-scope) only.

- tiers.ts: approve_message/reject_message → ADMIN_TOOLS (runtime 16→14).
- tests: agent scope now exposes 14 runtime tools; approve/reject asserted
  hidden from agent scope (in-process + over real HTTP).
- docs: §6a banner correction + mcp/README — approve/reject are account-only;
  agent scope can send (held) and view its pending queue, not release it.

NOTE: this is the MCP-surface half. The load-bearing fix — backend account-scope
enforcement on the approve/reject handlers (handleApprove/handleReject currently
use resolveOwnedAgent, which permits agent-scope self-approval) — is owned by the
HITL + message-screening workstream, not this PR. Until that lands, an e2a_agt_
key could still POST …/approve directly (MCP gating is UX, not the boundary).

123 MCP tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(api): require account scope to approve/reject (close HITL self-approval)

The load-bearing half of the approve/reject scope fix (the MCP tier move was the
surface half). handleApprove/handleReject now requireAccountScope FIRST — an
agent-scoped credential gets 403 forbidden, even on its own bound agent.
Previously they used resolveOwnedAgent (which permits agent-scope, pinned to the
agent), so the gated agent could approve its OWN held outbound — self-approval
that defeats the human-in-the-loop gate.

- internal/httpapi/hitl.go: requireAccountScope at the top of both handlers;
  reuse that principal's user (drops the redundant requireUser).
- The human magic-link flow (internal/agent/hitl_magic_api.go, token-gated) is a
  separate handler and is unaffected — humans still approve via the link.
- Test: TestScope_ApproveRejectIsAccountOnly — agent-scoped key POSTing
  approve/reject on its OWN bound agent gets 403 forbidden (the gate fires before
  any approve/reject dep). Existing account-scope approve/reject tests still 200
  (testServer's legacy Authenticator resolves to account scope).

Pairs with the MCP tier move (approve/reject → admin) earlier in this PR:
approve/reject are now account-scope end-to-end. Spec unchanged (scope is
runtime auth, not in the OpenAPI contract). httpapi + agent suites green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: approve/reject account-scope is now enforced in-PR (not deferred)

Sync the two forward-looking comments (tiers.ts + §6a banner) — the backend
account-scope enforcement landed in this PR (hitl.go), not a later workstream.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jiashuoz added a commit to Amerxu1002/e2accx that referenced this pull request Jul 17, 2026
Two claims in CLAUDE.md were already wrong before this branch, and the org
rename made the first one actively harmful.

1. The MCP release section documented `publish-mcp.yml`, which tokencanopy#247 deleted
   when MCP moved to hosted-only. Renaming the org rewrote that line to point
   at `tokencanopy/e2a` + `publish-mcp.yml` — a plausible-looking instruction
   to configure a trusted publisher for a workflow that does not exist and a
   package deliberately frozen at 0.4.0. Replaced with what actually ships.

   Confirmed via the signed npm provenance for @e2a/mcp-server@0.4.0, which
   records `.github/workflows/publish-mcp.yml` @ refs/tags/mcp-v0.4.0.

2. The workspace list omitted `mcp` and `design-system`; root package.json
   declares all four.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant