Skip to main content
BlogSecurity

MCP Write Controls: Cloudflare WriteGuard vs Approval Prompt

Cloudflare WriteGuard meneruskan, memberi atribusi, atau memblokir setiap write MCP di sisi server. Mengapa approval prompt bukan otorisasi, plus sketsa default-deny.

Matthews Wong

Software Engineer

Oktober 202612 menit baca
MCP Write Controls: Cloudflare WriteGuard vs Approval Prompt

Ringkasan Utama

Cloudflare WriteGuard adalah lapisan di sisi server pada MCP server portal Cloudflare yang meneruskan tool call apa adanya, menambahkan atribusi agent beserta audit event yang sudah di-scrub, atau memblokirnya sebelum handler berjalan. Lapisan ini ada karena approval prompt di sisi client berbeda-beda per harness dan bisa dimatikan pengguna, sehingga tidak bisa menjadi otorisasi.

Tiga background agent milik seorang engineer menutup ribuan ticket dalam satu sore karena prompt pembersihan yang terlalu luas. Cloudflare menyebut butuh sekitar setengah jam untuk menemukan penyebabnya, dan catatan audit tidak bisa memisahkan tindakan agent dari tindakan si engineer. Sore itulah alasan WriteGuard dibuat.

Tulisan ini adalah pembacaan teliti atas pengumuman WriteGuard dari Cloudflare tanggal 5 Agustus 2026, dibandingkan dengan liputan InfoQ, dokumentasi portal, dan spesifikasi MCP. Saya belum menjalankan WriteGuard karena masih private beta. Setiap baris kode di bawah yang melampaui apa yang dipublikasikan Cloudflare saya tandai sebagai buatan saya sendiri.

Sore yang membenarkan adanya lapisan policy

MCP server portal internal Cloudflare menghubungkan 13 MCP server pada bulan April dan 27 saat artikelnya terbit. Semua server awalnya read-only. Lalu tim engineering, product, design, sales, dan customer success meminta tool yang bisa menulis, dan tool write mengubah dampak prompt yang buruk dari satu jawaban salah menjadi banyak record yang salah.

Insiden ticket itu gagal dua kali, dan keduanya butuh perbaikan berbeda. Tidak ada yang membatasi apa yang boleh dilakukan agent, dan tidak ada yang mencatat agent milik siapa. Kesimpulan Cloudflare adalah mereka tidak bisa bergantung pada setiap karyawan untuk mengonfigurasi setiap agent dengan sempurna atau mengawasi setiap tool call, jadi kontrolnya harus berada di tempat yang tidak disentuh karyawan.

Mengapa approval prompt bukan otorisasi

Spesifikasi MCP menaruh keputusan di client, dan kewajibannya dirumuskan lunak. Halaman Tools menyebut harus SELALU ada human in the loop dengan kemampuan menolak tool invocation (SHOULD), dan client SHOULD meminta konfirmasi untuk operasi sensitif. Halaman Elicitation menyebut client SHOULD membolehkan pengguna menolak kapan saja. Halaman Tools yang sama menyebut client MUST menganggap tool annotation tidak tepercaya kecuali berasal dari server tepercaya. Itu desain yang benar untuk user experience tetapi tempat yang salah untuk policy, seperti terlihat di tabel.

LayerSiapa yang menegakkanBisakah pengguna mematikannyaApa yang diketahuinya tentang call
Confirmation prompt di clientAplikasi client, sebagai SHOULD di specTergantung harness; Cloudflare menyebut pengguna bisa menonaktifkan kontrol iniCall tersebut, hanya di client itu
Tool annotation (readOnlyHint, destructiveHint)Tidak ada: hanya hint yang boleh dibaca clientTidak berlaku; spec menyebut anggap tidak tepercaya kecuali servernya tepercayaPerilaku yang dideklarasikan, bukan yang diverifikasi
Access policy portal dan allowlist toolCloudflare Access di portal; admin bisa mematikan tool atau hanya membuka allowlistDiatur admin per server dan per portal, bukan oleh pengguna akhirServer dan tool mana yang boleh dijangkau seseorang, bukan penilaian risiko per call
WriteGuardPortal, sebelum handler tool berjalanPemilik server mengaturnya per tool; tujuan desainnya agar pengguna individual tidakSetiap call yang sampai kepadanya, lengkap dengan tier, outcome, user, dan client

Baca tabel ini sebagai lapisan, bukan pengganti. Prompt di client tetap membantu manusia di depan keyboard, dan Access tetap menentukan siapa melihat server mana. Yang ditambahkan WriteGuard adalah baris keempat: keputusan yang diambil di tempat yang tidak bisa dimatikan operator agent. Baris itu hanya berlaku untuk traffic yang benar-benar lewat portal, dan bagian 7 membahasnya lagi.

Tiga hasil: pass, attribute, block

Untuk setiap call, WriteGuard bisa meneruskannya apa adanya, memberi atribusi pada write yang didukung dengan menambahkan label agent dan mengirim audit event yang sudah di-scrub, atau memblokirnya sebelum handler tool berjalan. Ini berlaku untuk setiap MCP server di balik portal tanpa mengubah kode tiap server. Cloudflare memakai tiga tool GitLab untuk menunjukkan ketiga hasil tersebut.

ToolRisk tierApa yang dilakukan WriteGuardApa yang dicatat
get_merge_requestRead OnlyDiteruskan apa adanyaDicatat sesuai tier-nya; data audit bisa di-query per tier
create_mr_noteContained WriteMenambahkan atribusi agent pada field note dalam format yang didukung GitLab, lalu menjalankan handlerEvent ter-scrub berisi user, tool, outcome, dan identitas agent
merge_mrCritical, dikonfigurasi disabledDiblokir sebelum handler berjalanPercobaannya, dicatat sebagai blocked

Cloudflare menonaktifkan merge_mr karena merge biasanya memicu deployment pipeline dan manusia harus tetap dalam loop. Perhatikan arti disabled di sini: bukan bertanya ke orangnya, melainkan agent sama sekali tidak bisa melakukannya lewat jalur ini. Artikelnya tidak menjelaskan jalur approval-request untuk tool yang diblokir. Ia memblokir dan mencatat, lalu manusia melakukan merge di GitLab dengan kliknya sendiri.

Empat risk tier dan konfigurasi tiga pengaturan

Setiap tool mendapat tiga pengaturan: risk tier, flag enabled, dan konfigurasi labeling. Tier menentukan apakah sebuah aksi dicatat dan apakah call-nya diizinkan. Contoh Cloudflare untuk keempat tier adalah sebagai berikut.

  • Read Only: mencari issue, membaca merge request, melihat status pipeline.
  • Minimal Impact: menambah reaction, menandai notifikasi sudah dibaca, subscribe ke issue.
  • Contained Write: menambah komentar, membuat merge request, mengubah field issue.
  • Critical: melakukan merge, memicu deployment produksi, menghapus record secara massal.

Saat ini konfigurasinya berupa TypeScript di monorepo MCP internal Cloudflare, dan pemilik server diharapkan mengaturnya lewat MCP server portals nanti. Labeling menyisipkan atribusi ke field tertentu dalam format teks yang diterima aplikasi downstream; contoh di artikelnya menawarkan PLAIN_TEXT dan HTML. Satu perbedaan yang perlu diketahui: InfoQ menaruh penambahan komentar di Minimal Impact, sedangkan artikel Cloudflare menaruhnya di Contained Write dan contoh create_mr_note miliknya adalah Contained Write. Saya mengikuti artikel Cloudflare.

// Verbatim shape from Cloudflare's post: one tool, three settings
// (a risk tier, an enabled flag, and a labeling block).
const sendEmailTool = {
  tool: EmailMCP.sendEmailTool,
  writeGuard: {
    riskLevel: RiskLevel.CONTAINED_WRITE,
    enabled: true,
    labeling: {
      field: "body",
      supportedFormats: [LabelFormat.PLAIN_TEXT, LabelFormat.HTML],
    },
  },
};

// My reconstruction of the GitLab walkthrough in the same shape. The post
// describes these three outcomes in prose; it does not publish this config,
// and the labeling field below is a placeholder, not GitLab's real name.
const merge_mr = {
  tool: GitLabMCP.mergeMrTool,
  writeGuard: {
    riskLevel: RiskLevel.CRITICAL,
    enabled: false, // blocked before the handler runs; the attempt is audited
  },
};

Simpan daftar tier Anda sendiri sebagai file yang bisa di-review di version control. Tier adalah penilaian manusia, jadi tool berlabel Contained Write yang sebenarnya mengubah banyak record lolos persis seperti tool kecil. Review perubahan tier seperti Anda me-review perubahan permission. Ini saran saya, bukan dari Cloudflare.

Identitas: satu manusia, tanpa akun agent

Server internal mengidentifikasi pengguna lewat Cloudflare Access dan OAuth, dan agent bertindak dengan permission karyawan tersebut. Cloudflare memilih tidak membuat akun agent tersendiri, agar tidak ada set permission kedua dan tanggung jawab tetap jelas. Aplikasi downstream jadi melihat kredensial manusia, dan WriteGuard menambahkan konteks MCP client dan session sehingga setiap write terbaca sebagai sesi agent yang bertindak untuk orang tertentu.

Konsekuensinya adalah pewarisan: agent bisa melakukan semua yang bisa dilakukan orangnya, dan itulah mengapa insiden ticket bisa terjadi. Service account memberi atribusi yang lebih bersih tetapi menambah model permission kedua yang cenderung melenceng dari yang pertama. Cloudflare memilih satu model, dan menurut saya itu tepat untuk tool internal, dengan akibat bahwa guard adalah satu-satunya yang mempersempit agent di bawah manusianya.

Audit trail: successful, failed, blocked

Setiap invocation diklasifikasikan sebagai successful, failed, atau blocked, dan event yang sudah di-scrub dikirim secara asynchronous ke audit Worker internal sehingga tidak menambah latency pada respons agent. Nilai pada key yang dianggap rahasia dihilangkan. Event memuat server, tool, risk tier, outcome, user, client, dan durasi. Log request portal sudah menampilkan tool invocation; WriteGuard menambahkan klasifikasi semantik, konteks agent, dan outcome dari server backend. InfoQ meringkasnya sebagai mengirim request yang gagal dan ditolak ke auditing, sedangkan artikel Cloudflare menyebut ketiga outcome; saya mengikuti artikelnya. Kode di bawah adalah sketsa saya untuk pola yang sama.

// A sketch of the same pattern for a server you own. This is MY code, not
// WriteGuard's: the post publishes no implementation, only the behaviour.
type Tier = "READ_ONLY" | "MINIMAL_IMPACT" | "CONTAINED_WRITE" | "CRITICAL";
type Outcome = "successful" | "failed" | "blocked";
interface ToolPolicy { tier: Tier; enabled: boolean; labelField?: string }
interface Ctx { server: string; user: string; client: string; sessionId: string }

const POLICY: Record<string, ToolPolicy> = {
  get_merge_request: { tier: "READ_ONLY", enabled: true },
  create_mr_note: { tier: "CONTAINED_WRITE", enabled: true, labelField: "body" },
  merge_mr: { tier: "CRITICAL", enabled: false },
};

// The post says values under secret-looking keys are omitted. It does not say
// which fields carry values, so this logs argument keys with scrubbed values.
const SECRET_KEY = /token|secret|password|authorization|api[_-]?key/i;
const scrub = (args: Record<string, unknown>) =>
  Object.fromEntries(
    Object.entries(args).map(([key, v]) => [key, SECRET_KEY.test(key) ? "[omitted]" : v]),
  );

export async function guarded(
  ctx: Ctx,
  tool: string,
  args: Record<string, unknown>,
  handler: (a: Record<string, unknown>) => Promise<unknown>,
) {
  const started = Date.now();
  const policy = POLICY[tool];
  // Fire and forget: the audit write must never add latency to the agent.
  const emit = (outcome: Outcome) =>
    void fetch(process.env.AUDIT_URL!, {
      method: "POST",
      body: JSON.stringify({
        server: ctx.server, tool, tier: policy?.tier ?? "UNCLASSIFIED",
        outcome, user: ctx.user, client: ctx.client,
        durationMs: Date.now() - started, args: scrub(args),
      }),
    }).catch(() => undefined);

  // Default-deny is MY choice. The post does not say what happens to a tool
  // nobody classified; an unlisted tool is the case that should fail closed.
  if (!policy || !policy.enabled) {
    emit("blocked");
    throw new Error("blocked by write policy: " + tool);
  }

  const field = policy.labelField;
  const next =
    field && typeof args[field] === "string"
      ? { ...args, [field]: args[field] + "\n\n[written by an AI agent for " +
          ctx.user + " via " + ctx.client + ", session " + ctx.sessionId + "]" }
      : args;

  try {
    const result = await handler(next);
    emit("successful");
    return result;
  } catch (err) {
    emit("failed");
    throw err;
  }
}

Tiga hal dalam sketsa itu berbeda dari prompt di client. Pemblokiran terjadi sebelum handler, penulisan audit tidak di-await, dan tool yang tidak terdaftar ditolak. Pilihan terakhir itu keputusan saya: artikelnya tidak menyebut apa yang terjadi pada tool yang belum diklasifikasikan siapa pun. Fire-and-forget juga berarti event bisa hilang jika endpoint audit sedang mati, dan Cloudflare menyebut jaminan delivery audit yang dibutuhkan pelanggan sebagai hal yang masih harus divalidasi.

Batasnya: trade-off dan pertanyaan terbuka

WriteGuard adalah private beta untuk Cloudflare MCP server portals, dengan rollout bertahap dalam beberapa bulan ke depan. Cloudflare menyebut masih harus memvalidasi bagaimana risk model dipetakan ke tool pelanggan, aplikasi downstream mana yang butuh format atribusi mana, dan jaminan delivery audit seperti apa yang dibutuhkan pelanggan. InfoQ mencatat artikelnya tidak memberi angka overhead performa, harga, atau kriteria rinci penentuan tier selain contoh. Saya juga tidak menemukannya, jadi tidak ada yang bisa saya berikan.

Tiga celah muncul dari desain yang dijelaskan, dan ini inferensi saya, bukan klaim Cloudflare. Klasifikasi dilakukan per call dan secara manual, jadi jika tool yang menutup satu ticket digolongkan Contained Write, loop sepuluh ribu penutupan lolos sepuluh ribu pemeriksaan; artikelnya tidak menjelaskan batas volume. Guard hanya mengatur traffic yang sampai kepadanya. Dan dokumentasi portal mencantumkan batasan yang perlu diketahui: hanya remote HTTP server, maksimal 80 server per portal, dan tanpa MFA independen atau purpose justification untuk server yang diotorisasi lewat portal.

Guard hanya sekuat jalur yang melewatinya. Dokumentasi portal memperingatkan bahwa pengguna yang diblokir tetap bisa terhubung ke server lewat URL langsung dan melewati Access policy, kecuali Access dikonfigurasi sebagai OAuth provider server tersebut. Dokumen tidak menjelaskan perilaku WriteGuard pada koneksi langsung, jadi pastikan tidak ada server yang bisa menulis dan dapat dijangkau tanpa melewati portal.

Jalankan insiden ticket itu secara jujur. Dengan WriteGuard seperti yang dijelaskan, Anda akan melihat sesi agent mana yang bertindak untuk engineer mana, dan pemilik bisa menonaktifkan tool penutup ticket sepenuhnya. Anda tidak otomatis terhenti dari ribuan write yang masing-masing diizinkan. Yang didapat adalah atribusi dan kill switch; pembatasan laju masih harus Anda tambahkan sendiri.

Yang dilakukan hari Senin

Anda mungkin tidak pernah mendapat WriteGuard, dan polanya bisa dipindahkan. Lima langkah, berurutan.

  1. Daftar setiap tool yang bisa menulis di semua MCP server Anda dari hasil tools/list yang disimpan, dengan menerapkan default spec seperti di bawah.
  2. Beri setiap tool salah satu dari empat tier dalam file di version control, dan biarkan apa pun yang tidak bisa Anda klasifikasikan dalam keadaan disabled.
  3. Nonaktifkan tool yang efeknya memicu pipeline, deployment produksi, atau penghapusan massal, dan sisakan klik manusia untuk itu.
  4. Taruh pemeriksaan di gateway atau server, sebelum handler, tempat operator agent tidak bisa mematikannya. Pertahankan prompt di client sebagai lapisan tambahan, jangan sebagai policy.
  5. Catat server, tool, tier, outcome, user, client, dan durasi untuk setiap call, termasuk yang diblokir, dan baca percobaan yang diblokir setiap minggu.
# tools-list.json is a saved tools/list result. The inventory question is
# "which tools can write, and which of those might be destructive?"
# MCP spec defaults: readOnlyHint is false and destructiveHint is true, so a
# tool with NO annotations must be read as a destructive write.

# Wrong: jq treats false as "missing", so an explicit destructiveHint of
# false is replaced by true. A harmless additive tool is reported dangerous.
jq -r '.result.tools[] | [.name, (.annotations.destructiveHint // true)] | @tsv' tools-list.json
# get_merge_request  true
# create_mr_note     true    <- declared destructiveHint false, reported true
# merge_mr           true

# Right: test for null explicitly and apply the spec defaults.
jq -r '.result.tools[] | .annotations as $a
  | [.name, ($a.readOnlyHint // false),
     (if $a.destructiveHint == null then true else $a.destructiveHint end)]
  | @tsv' tools-list.json
# get_merge_request  true   true   (read-only; destructive is meaningless)
# create_mr_note     false  false  (additive write)
# merge_mr           false  true   (no annotations: assume destructive)

Jika Anda pelanggan Cloudflare, minta akses private beta dan pakai tiga pertanyaan terbuka dari bagian 7 sebagai daftar validasi Anda. Jika tidak, sketsa di atas sekitar lima puluh baris, tetapi tanpa test, tanpa batas laju, dan dengan scrubber sederhana, jadi anggap sebagai titik awal, bukan kode produksi.

Aturan yang saya bawa dari sini: otorisasi harus berada di tempat yang tidak bisa dimatikan pemanggilnya. Prompt bertanya, sedangkan tabel tier di sisi server memutuskan. Pertahankan keduanya jika mau, tetapi tulis policy hanya di yang kedua, gunakan default deny, dan pastikan setiap write menyebut agent milik siapa yang membuatnya.

FAQ

Pertanyaan Umum

Jawaban singkat untuk pertanyaan yang paling sering diajukan pembaca tentang topik ini.

Apa itu Cloudflare WriteGuard?

WriteGuard adalah lapisan policy, atribusi, dan auditing di MCP server portal Cloudflare. Untuk setiap tool call, ia bisa meneruskannya apa adanya, menambahkan atribusi agent beserta audit event yang di-scrub, atau memblokirnya sebelum handler tool berjalan. Saat ini masih private beta dan bekerja lintas server di balik portal tanpa mengubah kode tiap server.

Mengapa approval prompt di MCP client tidak cukup untuk keamanan write?

Spesifikasi MCP merumuskan konfirmasi manusia sebagai SHOULD untuk client, dan Cloudflare menyebut kontrol di sisi client berbeda per harness serta bisa dimatikan pengguna. Prompt bertanya kepada orangnya, sedangkan otorisasi harus diputuskan di tempat yang tidak bisa dimatikan orang itu. Karena itu WriteGuard menegakkan aturannya di portal sebelum handler berjalan.

Apa saja empat risk tier WriteGuard?

Read Only mencakup aksi seperti membaca merge request, Minimal Impact mencakup hal seperti menambah reaction atau subscribe ke issue, Contained Write mencakup menambah komentar atau membuat merge request, dan Critical mencakup merge, deployment produksi, serta penghapusan massal. Setiap tool diberi satu tier, flag enabled, dan konfigurasi labeling. Tier menentukan apakah aksi dicatat dan apakah diizinkan.

Apakah WriteGuard membuat akun terpisah untuk AI agent?

Tidak. Cloudflare sengaja tidak melakukannya karena akun agent tersendiri akan menambah satu set permission yang harus dikelola. Agent bertindak dengan identitas OAuth karyawan lewat Cloudflare Access, dan WriteGuard menambahkan konteks MCP client dan session agar setiap write bisa dikenali sebagai sesi agent yang bertindak untuk orang tertentu.

Apakah WriteGuard sudah tersedia, dan bisakah saya membuat yang serupa?

Saat ini masih private beta untuk Cloudflare MCP server portals, dengan rollout bertahap menuju general availability. Anda bisa menerapkan pola yang sama pada MCP server sendiri: klasifikasikan setiap tool ke sebuah tier, tolak apa pun yang belum diklasifikasikan, blokir tool yang disabled sebelum handler, dan catat setiap call beserta user dan client. Tambahkan juga batas volume, karena pemeriksaan per call saja tidak menghentikan loop write yang diizinkan.