Ringkasan Utama
A2A 1.0 adalah protokol Linux Foundation untuk satu agent memanggil agent lain. Agent memublikasikan Agent Card di /.well-known/agent-card.json, menerima message yang membuat task dengan state submitted, working, interrupted, dan terminal, mengirim update lewat Server-Sent Events atau webhook, serta menyediakan sebelas operasi yang sama lewat JSON-RPC, gRPC, atau HTTP+JSON.
Pertanyaan awal saya sempit: bisakah purchasing agent milik distributor menanyakan ke ERP kami apakah sebuah SKU tersedia di gudang tertentu, tanpa harus membuat API partner khusus lagi beserta PDF penjelasannya? Integrasi partner di ERP biasanya memang berupa pasangan itu, endpoint REST plus dokumen, dan keduanya cepat tidak sinkron begitu salah satunya berubah. Agent2Agent (A2A) menawarkan kontrak yang berbeda: agent mendeskripsikan dirinya dalam card yang bisa dibaca mesin, dan client mana pun yang patuh spesifikasi bisa menemukannya, melakukan autentikasi, lalu memberinya pekerjaan.
Tutorial protokol A2A ini membahas spesifikasi 1.0 dari sudut pandang developer yang harus mengimplementasikannya: Agent Card dan signature-nya, task lifecycle beserta state yang sering membuat bingung, streaming versus push notification, dan tiga binding standar. Di bagian akhir ada server minimal berbasis SDK JavaScript resmi yang membuka agent cek stok ERP, ditambah celah keamanan pada protokol itu sendiri yang ditemukan sebuah analisis pada September 2026. Semua nama field dan method di bawah berasal dari spesifikasi yang dipublikasikan atau dari source code SDK.
Apa yang Sebenarnya Distandarkan oleh Spesifikasi A2A 1.0
A2A diumumkan Google pada April 2025 dan diserahkan ke Linux Foundation pada Juni 2025; repository spesifikasinya memberi tag v1.0.0 pada Maret 2026 dan rilis perbaikan bug v1.0.1 pada Mei 2026. Spesifikasinya berlapis: data model kanonis yang didefinisikan dalam satu file Protocol Buffers sebagai satu-satunya sumber normatif, sekumpulan operasi abstrak, dan protocol binding yang memetakan operasi tersebut ke wire. Akibat praktisnya, sebelas operasi yang sama tersedia apa pun binding yang Anda pilih, dengan nama method identik di JSON-RPC dan gRPC.
| Operasi | Method JSON-RPC dan gRPC | Endpoint HTTP+JSON |
|---|---|---|
| Mengirim message | SendMessage | POST /message:send |
| Mengirim message dan streaming update | SendStreamingMessage | POST /message:stream |
| Membaca satu task | GetTask | GET /tasks/{id} |
| Daftar task, dengan cursor pagination | ListTasks | GET /tasks |
| Membatalkan task | CancelTask | POST /tasks/{id}:cancel |
| Subscribe ulang ke task yang berjalan | SubscribeToTask | POST /tasks/{id}:subscribe |
| Mendaftarkan webhook untuk task | CreateTaskPushNotificationConfig | POST /tasks/{id}/pushNotificationConfigs |
| Membaca satu konfigurasi webhook | GetTaskPushNotificationConfig | GET /tasks/{id}/pushNotificationConfigs/{configId} |
| Daftar konfigurasi webhook sebuah task | ListTaskPushNotificationConfigs | GET /tasks/{id}/pushNotificationConfigs |
| Menghapus konfigurasi webhook secara idempoten | DeleteTaskPushNotificationConfig | DELETE /tasks/{id}/pushNotificationConfigs/{configId} |
| Mengambil extended card yang terautentikasi | GetExtendedAgentCard | GET /extendedAgentCard |
Dua hal dalam tabel itu lebih penting daripada kelihatannya. ListTasks baru ada dibanding 0.3, dan spesifikasi mewajibkannya hanya mengembalikan task yang boleh dilihat caller terautentikasi, diurutkan berdasarkan update terakhir. Selain itu, setiap request sebaiknya membawa header A2A-Version: spesifikasi menyatakan nilai kosong dianggap 0.3, jadi client yang lupa mengirimnya diam-diam bernegosiasi memakai protokol lama dengan server yang masih mendukung keduanya.
Memublikasikan Agent Card di /.well-known/agent-card.json
Agent Card adalah kontrak publik sebuah agent, dan spesifikasi mendaftarkan well-known URI untuknya sehingga discovery cukup bermodal domain. Berikut card untuk agent cek stok, disajikan tanpa autentikasi karena client harus membacanya dulu sebelum tahu cara melakukan autentikasi.
GET /.well-known/agent-card.json HTTP/1.1
Host: stock-agent.example-erp.co.id
{
"name": "ERP Stock Check Agent",
"description": "Answers available-to-promise stock per SKU and warehouse for approved distributors.",
"version": "1.3.0",
"supportedInterfaces": [
{ "url": "https://stock-agent.example-erp.co.id/a2a/jsonrpc",
"protocolBinding": "JSONRPC", "protocolVersion": "1.0" },
{ "url": "https://stock-agent.example-erp.co.id/a2a/rest",
"protocolBinding": "HTTP+JSON", "protocolVersion": "1.0" }
],
"provider": { "organization": "Example Distribution", "url": "https://example-erp.co.id" },
"capabilities": {
"streaming": true,
"pushNotifications": true,
"extendedAgentCard": false
},
"securitySchemes": {
"partnerBearer": {
"httpAuthSecurityScheme": { "scheme": "bearer", "bearerFormat": "JWT" }
}
},
"securityRequirements": [ { "schemes": { "partnerBearer": { "list": [] } } } ],
"defaultInputModes": ["text/plain", "application/json"],
"defaultOutputModes": ["application/json"],
"skills": [
{
"id": "stock-availability",
"name": "Stock availability",
"description": "On-hand minus reserved quantity for one SKU in one warehouse.",
"tags": ["erp", "inventory", "stock"],
"examples": ["Is SKU BRG-10422 available in warehouse SBY-01?"]
}
],
"signatures": [ { "protected": "eyJhbGciOiJFUzI1NiIs...", "signature": "QFdkNLNs..." } ]
}- supportedInterfaces adalah daftar berurutan, dan entri pertama adalah yang diutamakan. Client wajib memilih entri pertama yang binding-nya didukung dan memakai URL entri tersebut, jadi urutannya adalah keputusan routing, bukan kosmetik.
- Setiap interface mendeklarasikan protocolVersion-nya sendiri. Dengan cara itu satu card bisa mengiklankan endpoint 1.0 berdampingan dengan endpoint lama 0.3 selama masa migrasi.
- securitySchemes mendeklarasikan cara autentikasi, dan securityRequirements menyebutkan scheme mana yang berlaku. Credential-nya sendiri diperoleh di luar jalur; card tidak pernah membawanya.
- Field wajibnya adalah name, description, version, supportedInterfaces, capabilities, defaultInputModes, defaultOutputModes, dan skills. Sebuah skill butuh id, name, description, dan tags; examples bersifat opsional, tetapi justru itulah yang dibaca planner milik agent lain.
Sajikan card dengan Cache-Control max-age dan ETag yang diturunkan dari versi card atau hash isinya, sesuai rekomendasi spesifikasi, supaya client melakukan revalidasi dengan If-None-Match alih-alih mengunduhnya di setiap panggilan. Jangan cantumkan hostname internal atau apa pun yang sensitif: card publik, sesuai definisinya, bisa dibaca siapa saja, dan spesifikasi mengingatkan bahwa extended card yang terautentikasi pun sebaiknya tidak memuat URL layanan internal.
Menandatangani Agent Card dengan JWS dan RFC 8785
Agent Card boleh ditandatangani dengan JSON Web Signature, RFC 7515, agar client bisa memastikan card tidak diubah selama transit atau oleh cache. Bagian sulitnya bukan kriptografi, melainkan menghasilkan byte yang sama di kedua sisi; karena itu spesifikasi mewajibkan JSON Canonicalization Scheme, RFC 8785, setelah tahap pembuangan nilai default. Prosedur penandatanganannya:
- Buang properti yang berisi nilai default, mengikuti aturan field presence Protocol Buffers: field opsional yang tidak di-set dibuang, field opsional yang di-set secara eksplisit dipertahankan, dan field wajib selalu ada.
- Keluarkan field signatures itu sendiri, supaya signature tidak pernah menandatangani dirinya sendiri.
- Kanonisasi sisanya dengan RFC 8785: key diurutkan secara leksikografis, whitespace yang tidak bermakna dihapus, angka dan string dalam satu bentuk baku.
- Buat protected header berisi alg, typ bernilai JOSE, dan kid, opsional jku yang menunjuk ke JWKS, lalu tandatangani header base64url yang digabung dengan titik ke payload base64url.
- Simpan protected header dan signature dalam base64url sebagai satu entri di array signatures; beberapa entri memungkinkan rotasi key tanpa jeda.
// Before canonicalisation (as served):
{
"name": "Example Agent",
"description": "",
"capabilities": { "streaming": false, "pushNotifications": false, "extensions": [] },
"skills": []
}
// After default-stripping + RFC 8785 JCS -- this exact byte string is what gets signed.
// "extensions": [] is gone (repeated, not REQUIRED); "description": "" stays (REQUIRED);
// streaming:false stays because it was explicitly set on an optional field.
{"capabilities":{"pushNotifications":false,"streaming":false},"description":"","name":"Example Agent","skills":[]}
// Protected header, base64url-decoded:
{"alg":"ES256","typ":"JOSE","kid":"key-1","jku":"https://example.com/agent/jwks.json"}Verifikasi adalah kebalikannya: buang nilai default, keluarkan signatures, kanonisasi, ambil public key berdasarkan kid dan jku lewat HTTPS atau dari trusted key store, lalu periksa. SDK JavaScript resmi menyediakan helper canonicalizeAgentCard dan verifyAgentCardSignature serta hook penandatanganan di request handler-nya, jadi Anda tidak perlu menulis JCS sendiri. Yang sering menjebak adalah melakukan serialisasi card lewat library yang menambahkan array kosong atau boolean default yang dibuang oleh penandatangan, sehingga card terlihat valid tetapi signature-nya tidak pernah lolos verifikasi.
Signature yang valid membuktikan siapa yang memublikasikan card, bukan bahwa skill yang tercantum nyata atau aman. Analisis A2ABreak mencatat klaim skill tanpa atestasi dan celah antara kepercayaan key dan klaim kemampuan sebagai dua temuan terpisah. Perlakukan card bertanda tangan sebagai iklan yang terautentikasi, dan kunci domain jku ke provider yang benar-benar terikat kontrak dengan Anda.
Task Lifecycle A2A dan Sembilan State-nya
Message adalah satu giliran; Task adalah unit kerja ber-state yang bisa dibuat oleh sebuah message. Server boleh menjawab SendMessage dengan Message langsung untuk request sederhana, atau dengan Task yang dilacaknya. TaskState mendefinisikan sembilan nilai, yang pertama TASK_STATE_UNSPECIFIED, dan delapan sisanya terbagi menjadi tiga kelompok perilaku yang harus diperlakukan berbeda oleh kode client Anda.
| State | Jenis | Yang harus dilakukan client |
|---|---|---|
TASK_STATE_SUBMITTED | Aktif | Sudah diterima, belum dimulai. Biarkan stream terbuka atau lakukan polling. |
TASK_STATE_WORKING | Aktif | Sedang diproses. Tunggu event status dan artifact. |
TASK_STATE_INPUT_REQUIRED | Interrupted | Baca status message, lalu balas pada taskId yang sama. |
TASK_STATE_AUTH_REQUIRED | Interrupted | Dapatkan credential di luar jalur, lalu subscribe, polling, atau balas. |
TASK_STATE_COMPLETED | Terminal | Baca artifact-nya. Task tidak menerima message lagi. |
TASK_STATE_FAILED | Terminal | Catat status message-nya. Retry berarti task baru. |
TASK_STATE_CANCELED | Terminal | Pastikan pembatalan berhasil. CancelTask adalah permintaan, bukan jaminan. |
TASK_STATE_REJECTED | Terminal | Agent menolak. Jangan mengulang input yang sama begitu saja. |
Mode blocking adalah default. Kecuali client menyetel returnImmediately ke true di konfigurasi pengiriman, SendMessage wajib menunggu sampai task mencapai state terminal atau interrupted sebelum mengembalikan respons. Itu tidak masalah untuk cek stok, tetapi fatal untuk rekonsiliasi tiga jam di belakang load balancer dengan timeout 60 detik; jadi agent yang berjalan lama sebaiknya dipanggil secara non-blocking, dengan stream, polling, atau webhook yang membawa hasilnya. Mengirim message ke task yang sudah terminal menghasilkan UnsupportedOperationError; pertanyaan lanjutan setelah selesai harus menjadi task baru di contextId yang sama.
Versi 1.0 menghapus discriminator kind. Di 0.3, text part adalah objek dengan kind bernilai text; di 1.0 nama member-lah yang menjadi discriminator, sehingga text part cukup berupa objek dengan member text, dan event stream datang terbungkus sebagai statusUpdate atau artifactUpdate. Bentuk nilai enum juga berubah, menjadi TASK_STATE_COMPLETED dan ROLE_USER. Parser 0.3 yang ditulis manual akan gagal pada event 1.0 pertama, jadi periksa setiap client yang tidak Anda kendalikan sebelum mematikan mode kompatibilitas.
Membangun Server A2A Minimal untuk Cek Stok ERP
SDK resmi @a2a-js/sdk mencapai lini stabil v1.0 pada Juli 2026 dan mengimplementasikan ketiga binding di balik satu DefaultRequestHandler. Logika Anda berada di AgentExecutor, yang menerima RequestContext dan memublikasikan event Task, status, dan artifact ke event bus. Executor di bawah adalah seluruh agent-nya: menolak partner yang tidak dikenal, meminta input yang kurang alih-alih gagal, memeriksa akses gudang, dan mengembalikan jawaban sebagai data part terstruktur.
// stock-executor.ts -- npm install @a2a-js/sdk express
import { Task, TaskState, Role, Part, Message } from "@a2a-js/sdk";
import {
AgentExecutor, AgentEvent, RequestContext, ExecutionEventBus,
} from "@a2a-js/sdk/server";
import { erp } from "./erp-client"; // your existing inventory service
import { PartnerUser } from "./partner-auth";
const SKU = /\b[A-Z]{3}-\d{4,6}\b/;
const WAREHOUSE = /\b[A-Z]{3}-\d{2}\b/;
const text = (value: string): Part => ({
content: { $case: "text", value }, metadata: undefined, filename: "", mediaType: "text/plain",
});
function agentMessage(ctx: RequestContext, value: string): Message {
return {
role: Role.ROLE_AGENT, messageId: crypto.randomUUID(), parts: [text(value)],
taskId: ctx.taskId, contextId: ctx.contextId, extensions: [], metadata: {}, referenceTaskIds: [],
};
}
export class StockExecutor implements AgentExecutor {
async execute(ctx: RequestContext, bus: ExecutionEventBus): Promise<void> {
const { taskId, contextId } = ctx;
const status = (state: TaskState, note?: string) =>
bus.publish(AgentEvent.statusUpdate({
taskId, contextId, metadata: undefined,
status: { state, timestamp: new Date().toISOString(),
message: note ? agentMessage(ctx, note) : undefined },
}));
// A streaming turn MUST open with a Task (or a single Message).
const snapshot: Task = ctx.task ?? {
id: taskId, contextId, artifacts: [], history: [ctx.userMessage], metadata: {},
status: { state: TaskState.TASK_STATE_SUBMITTED, timestamp: new Date().toISOString(),
message: undefined },
};
bus.publish(AgentEvent.task(snapshot));
const partner = ctx.context?.user;
if (!(partner instanceof PartnerUser)) {
return status(TaskState.TASK_STATE_REJECTED, "Unknown partner.");
}
// Read every text part across the task history, so a follow-up turn that
// only says "SBY-01" still finds the SKU sent in the first turn.
const said = [...(ctx.task?.history ?? []), ctx.userMessage]
.flatMap((m) => m.parts)
.map((p) => (p.content?.$case === "text" ? p.content.value : ""))
.join(" ");
const sku = said.match(SKU)?.[0];
const warehouse = said.match(WAREHOUSE)?.[0];
// Interrupted, not failed: the same taskId continues when the partner answers.
if (!sku || !warehouse) {
return status(TaskState.TASK_STATE_INPUT_REQUIRED,
"Send the SKU (e.g. BRG-10422) and warehouse code (e.g. SBY-01).");
}
// Authorise against the partner's contract, not against what the card advertises.
if (!partner.warehouses.includes(warehouse)) {
return status(TaskState.TASK_STATE_REJECTED, `No access to warehouse ${warehouse}.`);
}
status(TaskState.TASK_STATE_WORKING);
const { onHand, reserved } = await erp.stockLevel(sku, warehouse);
bus.publish(AgentEvent.artifactUpdate({
taskId, contextId, append: false, lastChunk: true, metadata: undefined,
artifact: {
artifactId: crypto.randomUUID(), name: "stock-level", description: "",
metadata: undefined, extensions: [],
parts: [{
content: { $case: "data",
value: { sku, warehouse, onHand, reserved, available: onHand - reserved } },
metadata: undefined, filename: "", mediaType: "application/json",
}],
},
}));
status(TaskState.TASK_STATE_COMPLETED);
}
// Stock lookups finish in one round trip; there is nothing to abort mid-flight.
cancelTask = async (): Promise<void> => {};
}Ada tiga keputusan yang disengaja di sini. Input yang kurang menjadi TASK_STATE_INPUT_REQUIRED, bukan FAILED, karena task yang interrupted tetap memegang taskId-nya dan agent milik partner cukup membalas. Otorisasi diperiksa terhadap kontrak partner di dalam executor, karena card mengiklankan apa yang bisa dilakukan agent untuk siapa saja, bukan apa yang boleh dilakukan caller ini. Dan hasilnya berupa data part dengan mediaType application/json, sehingga agent pemanggil mem-parsing angka, bukan kalimat. Menghubungkannya ke HTTP hanya butuh beberapa baris:
// server.ts
import express from "express";
import { AGENT_CARD_PATH } from "@a2a-js/sdk";
import { DefaultRequestHandler, InMemoryTaskStore } from "@a2a-js/sdk/server";
import { agentCardHandler, jsonRpcHandler, restHandler } from "@a2a-js/sdk/server/express";
import { stockAgentCard } from "./agent-card"; // the card shown above, as an AgentCard
import { StockExecutor } from "./stock-executor";
import { requirePartnerToken, partnerUserBuilder } from "./partner-auth";
// InMemoryTaskStore loses every task on restart -- an INPUT_REQUIRED task the
// partner answers after a deploy becomes TaskNotFoundError (-32001).
// Swap in DatabaseTaskStore from @a2a-js/sdk/server/database for production.
const handler = new DefaultRequestHandler(stockAgentCard, new InMemoryTaskStore(), new StockExecutor());
const app = express();
// The public card stays unauthenticated: discovery happens before credentials exist.
app.use(`/${AGENT_CARD_PATH}`, agentCardHandler({ agentCardProvider: handler }));
// Everything else requires the partner's bearer token (spec 7.4: authenticate EVERY request).
app.use("/a2a", requirePartnerToken);
app.use("/a2a/jsonrpc", jsonRpcHandler({ requestHandler: handler, userBuilder: partnerUserBuilder }));
app.use("/a2a/rest", restHandler({ requestHandler: handler, userBuilder: partnerUserBuilder }));
app.listen(8080);Modul partner-auth, yang tidak ditampilkan, berisi middleware Express biasa yang memvalidasi bearer token dan sebuah UserBuilder yang mengubah request tervalidasi menjadi objek PartnerUser berisi id partner dan daftar gudang yang diizinkan; sample autentikasi milik SDK memakai pola yang sama dengan Passport. Memasang jsonRpcHandler dan restHandler pada request handler yang sama berarti satu card bisa mencantumkan kedua interface tanpa logika ganda. Binding gRPC dipasang dengan cara serupa lewat grpcService dari subpath gRPC, tetapi membutuhkan peer dependency grpc-js dan protobuf serta hanya berjalan di Node.
Streaming, Push Notification, dan Memilih Binding
Streaming di binding JSON-RPC dan HTTP+JSON memakai Server-Sent Events. Setiap event adalah StreamResponse yang berisi tepat satu dari task, message, statusUpdate, atau artifactUpdate, dan stream sebuah task wajib diawali objek Task lalu ditutup ketika task mencapai state terminal. Jika koneksi terputus, SubscribeToTask menyambung kembali dan, menurut spesifikasi, mengirim Task terkini lebih dulu sehingga tidak ada yang hilang di antara GetTask dan stream baru.
curl -N https://stock-agent.example-erp.co.id/a2a/jsonrpc \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $PARTNER_TOKEN" \
-H "A2A-Version: 1.0" \
-d '{
"jsonrpc": "2.0", "id": 1, "method": "SendStreamingMessage",
"params": { "message": {
"messageId": "9b7c1f0e-0d5a-4f43-9d1e-2a1f6b1f3c11", "role": "ROLE_USER",
"parts": [ { "text": "Is BRG-10422 available in SBY-01?" } ] } }
}'
# Content-Type: text/event-stream -- one StreamResponse per event, exactly one member set:
data: {"jsonrpc":"2.0","id":1,"result":{"task":{"id":"t-81","contextId":"c-12","status":{"state":"TASK_STATE_SUBMITTED"}}}}
data: {"jsonrpc":"2.0","id":1,"result":{"statusUpdate":{"taskId":"t-81","contextId":"c-12","status":{"state":"TASK_STATE_WORKING"}}}}
data: {"jsonrpc":"2.0","id":1,"result":{"artifactUpdate":{"taskId":"t-81","contextId":"c-12","lastChunk":true,"artifact":{"artifactId":"a-1","name":"stock-level","parts":[{"data":{"sku":"BRG-10422","warehouse":"SBY-01","onHand":340,"reserved":120,"available":220},"mediaType":"application/json"}]}}}}
data: {"jsonrpc":"2.0","id":1,"result":{"statusUpdate":{"taskId":"t-81","contextId":"c-12","status":{"state":"TASK_STATE_COMPLETED"}}}}
# stream closes on the terminal stateKetika caller tidak bisa mempertahankan koneksi, push notification mengirim bentuk StreamResponse yang sama ke webhook. Konfigurasinya bisa ikut dalam request pengiriman, seperti contoh di bawah lewat binding REST, atau didaftarkan belakangan dengan CreateTaskPushNotificationConfig. Pengirimannya at-least-once, jadi penerima wajib menjawab dengan status 2xx, menganggap duplikat sebagai hal normal, dan memeriksa bahwa taskId memang milik task yang dibuatnya.
# Same operation over the HTTP+JSON binding, fire-and-forget with a webhook.
POST /a2a/rest/message:send HTTP/1.1
Content-Type: application/a2a+json
Authorization: Bearer <partner token>
A2A-Version: 1.0
{
"message": { "messageId": "4e0d...", "role": "ROLE_USER",
"parts": [ { "text": "Reorder check: BRG-10422 in SBY-01" } ] },
"configuration": {
"returnImmediately": true, // default false = block until terminal/interrupted
"taskPushNotificationConfig": {
"url": "https://partner.example.com/a2a/webhook",
"token": "per-task-random-value", // echo-check this on every delivery
"authentication": { "scheme": "Bearer", "credentials": "<webhook secret>" }
}
}
}
# The agent later POSTs the same StreamResponse shapes to the webhook:
POST /a2a/webhook Content-Type: application/a2a+json Authorization: Bearer <webhook secret>
{ "statusUpdate": { "taskId": "t-82", "contextId": "c-12", "status": { "state": "TASK_STATE_COMPLETED" } } }- JSON-RPC 2.0 adalah titik awal yang tepat: satu endpoint POST, nama method PascalCase, application/json, SSE untuk stream, dan error khusus A2A di rentang -32001 sampai -32099, dengan TaskNotFoundError di -32001.
- HTTP+JSON memetakan operasi ke URL resource dengan application/a2a+json, dan error-nya dipetakan ke status HTTP, 404 untuk task yang tidak ada dan 400 untuk sebagian besar kegagalan precondition. Pilih ini ketika caller berada di belakang API gateway yang melakukan routing dan rate limit berdasarkan path.
- gRPC memakai Protocol Buffers di atas HTTP/2 dengan nama method yang sama. Pilih ini untuk mesh agent internal yang kedua ujungnya sudah memakai gRPC, bukan untuk partner di internet terbuka di belakang proxy yang belum tentu meneruskan HTTP/2 dengan bersih.
Tidak ada yang melarang Anda membuka ketiganya; spesifikasi hanya meminta setiap interface yang tercantum setara secara fungsional. Untuk agent yang menghadap partner, taruh JSON-RPC di urutan pertama karena paling mudah di-debug dengan curl, dan tambahkan REST di urutan kedua hanya jika gateway partner membutuhkan routing berbasis path. Setiap binding tambahan adalah permukaan baru yang harus diautentikasi dan diuji.
Celah Keamanan A2A yang Diserahkan Spesifikasi kepada Anda
A2ABreak, paper yang diunggah ke arXiv pada September 2026 oleh tim peneliti termasuk Elisa Bertino, memodelkan spesifikasi A2A sebagai state machine dengan 37 state dan 76 transisi, lalu melaporkan 11 kerentanan yang bisa dieksploitasi oleh penyerang yang mengikuti spesifikasi secara persis. Ini bukan bug implementasi yang hilang dengan meng-upgrade library; ini titik-titik di mana server yang patuh spesifikasi tetap tidak aman kecuali Anda menambahkan kontrol. Yang paling relevan untuk agent ERP yang menghadap partner:
- Cross-client context injection: contextId adalah identifier yang tidak dilindungi, jadi ikat setiap context dan task ke principal terautentikasi yang membuatnya dan tolak message dari pihak lain.
- URL webhook yang tidak diverifikasi: agent yang mengirim POST ke URL apa pun yang diberikan client adalah primitif SSRF. Spesifikasi sudah meminta penolakan rentang privat seperti 10.0.0.0/8 dan 192.168.0.0/16, alamat loopback dan link-local, serta allowlist bila memungkinkan.
- Multi-hop identity loss: identitas hidup di transport layer, jadi ketika agent Anda mendelegasikan ke agent ketiga, identitas partner asli tidak ikut terbawa. Jangan biarkan agent hilir menyimpulkan wewenang hanya karena agent Anda yang memanggilnya.
- Auth scope amplification: credential yang diberikan untuk menyelesaikan TASK_STATE_AUTH_REQUIRED tidak boleh memperluas apa yang boleh dilakukan task. Spesifikasi menegaskan bahwa transisi state itu sendiri tidak mengotorisasi apa pun.
- Tidak ada timeout dari state interrupted: task INPUT_REQUIRED bisa menunggu selamanya dan menahan resource. Beri masa kedaluwarsa sendiri pada task yang interrupted dan pindahkan ke state terminal.
Sisa daftarnya mencakup klaim skill tanpa atestasi, signature yang mengautentikasi identitas tetapi bukan kemampuan, stream SSE yang terus bocor setelah pencabutan akses, chunk artifact yang dirakit ulang tanpa pemeriksaan integritas, race condition modifikasi bersamaan, dan delegasi melingkar. Benang merahnya: A2A mendefinisikan cara agent berbicara, dan sengaja menyerahkan urusan siapa boleh melakukan apa kepada setiap implementasi. Bagian 13.1 spesifikasi menyatakannya dengan jelas: pemeriksaan otorisasi wajib dilakukan di setiap operasi dan sebelum query apa pun yang bisa mengungkap ada tidaknya sebuah resource.
Batasi GetTask dan ListTasks ke caller sebelum menyentuh database. Mengembalikan TaskNotFoundError untuk task milik partner lain sudah benar; mengembalikan error izin justru memberi tahu caller bahwa taskId itu ada. Dengan task ID yang berurutan atau mudah ditebak, perbedaan itu mengubah agent stok Anda menjadi oracle untuk volume order pesaing.
A2A 1.0 memberi tim ERP sesuatu yang tidak pernah diberikan endpoint REST plus PDF: kontrak yang mendeskripsikan dirinya sendiri, bisa ditandatangani, dengan task lifecycle yang nyata dan pilihan transport. Yang tidak diberikannya adalah otorisasi, masa kedaluwarsa, atau identitas lintas hop, jadi aturan yang saya pegang sederhana: biarkan protokol membawa percakapannya, dan simpan setiap keputusan tentang siapa boleh melihat stok yang mana di kode Anda sendiri, diperiksa di setiap panggilan.
Sumber
- A2A Protocol Specification, rilis terbaru 1.0.0 (a2a-protocol.org)
- Rilis a2aproject/A2A: v1.0.0 dan v1.0.1 (GitHub)
- a2aproject/a2a-js: SDK JavaScript resmi untuk A2A v1.0 (GitHub)
- Agent2Agent (Wikipedia)
- Lotfi, Rahman, Karim, dan Bertino, A2ABreak: Systematic Security Analysis of the A2A Protocol (arXiv 2609.10871)

