Petugas menyetujui sebuah draf. Agen kemudian mengubah pesan sebelum mengirimnya. Jika persetujuan hanya disimpan sebagai nilai boolean pada percakapan, aplikasi bisa mengirim sesuatu yang belum pernah ditinjau petugas.
Latihan ini mengikat persetujuan pada satu tindakan spesifik dan menguji batasnya dengan data sintetis. Kode hanya memakai modul bawaan Node.js, tidak membuat permintaan jaringan, dan tidak menulis berkas saat dijalankan. Array lokal menggantikan proses pengiriman. Jalankan sebagai latihan di lingkungan yang Anda kendalikan.
1. Tentukan tindakan dan batas kepercayaan
Siapkan Node.js 22 atau lebih baru. Simpan kode berikut sebagai approval-gate.mjs. Agen dapat mengusulkan tindakan send_reply dengan penerima dan isi pesan. Aplikasi tepercaya menerbitkan persetujuan setelah peninjauannya sendiri; approve bukan alat yang diberikan kepada agen.
Catatan persetujuan tersimpan dalam map privat dan kedaluwarsa setelah satu menit. Digest mencakup tindakan yang sudah dinormalisasi, sehingga perubahan penerima atau pesan membatalkan kecocokan persetujuan. Kode memakai alamat sintetis yang ditentukan secara persis, tanpa mencoba menerapkan pengiriman atau validasi alamat email umum.
import assert from 'node:assert/strict';
import { createHash, randomUUID } from 'node:crypto';
const recipients = new Set(['customer@example.test', 'reviewer@example.test']);
const approvals = new Map();
const delivered = [];
function normalize(input) {
assert(input && typeof input === 'object' && !Array.isArray(input), 'invalid action');
assert.deepEqual(Object.keys(input).sort(), ['body', 'operation', 'recipient']);
assert.equal(input.operation, 'send_reply', 'operation not allowed');
assert(recipients.has(input.recipient), 'recipient not allowed');
assert(typeof input.body === 'string' && input.body.trim().length > 0, 'empty body');
assert(input.body.length <= 2000, 'body too long');
return { operation: input.operation, recipient: input.recipient, body: input.body };
}
const digest = (action) => createHash('sha256').update(JSON.stringify(action)).digest('hex');
function approve(input, now = Date.now()) {
const action = normalize(input);
const approvalId = randomUUID();
approvals.set(approvalId, { digest: digest(action), expiresAt: now + 60_000 });
return approvalId;
}
function send(input, approvalId, now = Date.now()) {
const action = normalize(input);
const approval = approvals.get(approvalId);
assert(approval && now < approval.expiresAt, 'missing or expired approval');
assert.equal(digest(action), approval.digest, 'action changed');
// ponytail: synchronous, single-process simulation; use durable atomic state before real delivery.
approvals.delete(approvalId);
delivered.push(action);
}
const now = Date.parse('2026-10-16T02:00:00Z');
const draft = {
operation: 'send_reply',
recipient: 'customer@example.test',
body: 'Your support request is ready for review.',
};
const approvalId = approve(draft, now);
assert.throws(() => send(draft, 'unknown', now), /missing or expired/);
assert.throws(() => send({ ...draft, body: 'A different message.' }, approvalId, now), /action changed/);
assert.throws(() => send({ ...draft, recipient: 'reviewer@example.test' }, approvalId, now), /action changed/);
assert.throws(() => send({ ...draft, recipient: 'other@example.test' }, approvalId, now), /recipient not allowed/);
assert.throws(() => send({ ...draft, operation: 'delete_account' }, approvalId, now), /operation not allowed/);
assert.throws(() => send({ ...draft, body: null }, approvalId, now), /empty body/);
assert.throws(() => send({ ...draft, attachment: 'private.txt' }, approvalId, now));
assert.equal(delivered.length, 0, 'denied actions must have no delivery effect');
const expiredId = approve(draft, now);
assert.throws(() => send(draft, expiredId, now + 60_000), /missing or expired/);
assert.equal(delivered.length, 0);
send(draft, approvalId, now + 1);
assert.deepEqual(delivered, [draft]);
assert.throws(() => send(draft, approvalId, now + 2), /missing or expired/);
assert.equal(delivered.length, 1, 'replay must not duplicate delivery');
console.log('PASS: approved action delivered once; invalid, changed, expired, and replayed actions blocked.');
2. Jalankan pemeriksaan
Jalankan berkas secara lokal:
node approval-gate.mjs
Keluaran yang diharapkan:
PASS: approved action delivered once; invalid, changed, expired, and replayed actions blocked.
Assertion memeriksa akibat yang dapat diamati: pemanggilan yang ditolak tidak mengisi array pengiriman, pemanggilan sah menambahkan tepat tindakan yang ditinjau, dan pengulangan mempertahankan jumlah pengiriman sebanyak satu. Pemeriksaan tidak bergantung pada pengakuan model tentang tindakannya.
Contoh ini sengaja tidak memanggil model. Yang diuji secara deterministik adalah batas keputusan aplikasi. Pengujian seberapa sering model mengusulkan tindakan tidak aman merupakan evaluasi yang terpisah.
3. Buktikan bahwa pemeriksaan menangkap kerusakan
Pada salinan sementara, hapus perbandingan digest di dalam send, lalu jalankan kembali. Uji perubahan pesan harus gagal karena mengharapkan penolakan. Kembalikan baris tersebut. Kemudian hapus approvals.delete(approvalId) pada salinan sementara: uji pengulangan harus gagal.
Jangan gunakan salinan yang diubah untuk alur kerja nyata. Perubahan kecil ini membuktikan bahwa assertion memeriksa keterikatan isi dan persetujuan sekali pakai, bukan sekadar lulus karena semua pemanggilan ditolak.
4. Pahami batas contoh ini
Digest merupakan sidik jari data, bukan tanda tangan atau pengganti autentikasi. Keputusan keamanan bergantung pada map persetujuan yang dikendalikan aplikasi tepercaya. Jika agen bisa memanggil approve atau mengubah map, batas tersebut hilang.
Map ini juga tidak bertahan setelah proses dimulai ulang dan tidak dibagikan antar-worker. Sebelum mengadaptasinya untuk produksi, autentikasi pemberi persetujuan, kaitkan persetujuan dengan tugas serta pelaksana, dan periksa pencabutan akses saat eksekusi. Gunakan penyimpanan persisten dengan perubahan status atomik agar persetujuan tidak dikonsumsi bersamaan.
Pengiriman nyata menambah persoalan: layanan dapat menerima permintaan meskipun klien mengalami timeout. Menghapus persetujuan sebelum pemanggilan jaringan bisa meninggalkan tindakan tanpa hasil pasti; menghapusnya setelah pemanggilan bisa membuka peluang duplikasi. Gunakan kunci idempotensi yang didukung penyedia, status operasi yang tersimpan, dan pencocokan ulang untuk hasil yang belum pasti. Penampung dalam memori pada latihan ini tidak menyelesaikan persoalan sistem terdistribusi tersebut.
Tempatkan latihan ini bersama rencana uji keamanan agen yang lebih luas dan pembatasan izin alat. Yang dibuktikan adalah sifat kecil: pada program lokal sinkron ini, satu persetujuan mengizinkan tepat satu tindakan yang tidak berubah sebelum kedaluwarsa. Pertahankan batas klaim itu ketika mengembangkannya.
