Mert Özen Her Satırda Daha İleri
Yapay Zeka LLM Uygulama Güvenliği — #04

Excessive Agency: Tool Yetkisini Daraltmak, Kullanıcı Adına Çalışmak ve Otonomiyi Sınırlamak

Mert Özen 14 Kas 2026 15 dk 2 görüntülenme
Excessive Agency: Tool Yetkisini Daraltmak, Kullanıcı Adına Çalışmak ve Otonomiyi Sınırlamak

Excessive Agency'nin çaresi modeli daha akıllı yapmak değil, elindeki tool'ları, yetkileri ve otonomiyi daraltmaktır. Bookstore asistanında iptal tool'unu önizleme ve kullanıcı onayına bölüyor, PostgreSQL'de yalnız SELECT yetkili bir rol, kullanıcı token'ıyla downstream çağrı, tutar eşiği, rate limit, FunctionInvoker ile çağrı bütçesi ve audit trail kuruyor, qwen3:4b'nin "tüm siparişlerimi iptal et" isteğindeki gerçek davranışını izliyoruz.

Sürüm notu: Bu yazıdaki kodlar .NET 10 ile derlenip denenmiştir; Microsoft.Extensions.AI 11.0.0, OllamaSharp 5.5.2, PostgreSQL 17 ve yerel modelde Ollama 0.34.4 (qwen3:4b) kullanılmıştır. Kullanıcı token'ını downstream'e taşıyan handler derlendi ama çalıştırılmadı.

Önce Tahmin Et

Bookstore.Assistant'a sipariş yönetimi eklenecek. İlk taslak hızlıca yazılmış: tek bir manage_orders(action, orderId) tool'u var, action "status", "cancel" ya da "refund" olabiliyor. Tool veritabanına uygulamanın servis hesabıyla bağlanıyor; o hesabın her tabloda okuma ve yazma yetkisi var. Model "cancel" dediğinde iptal hemen gerçekleşiyor, çünkü "kullanıcı zaten istedi".

Ada beş siparişi olan bir müşteri ve şunu yazıyor: "Please cancel all of my orders." Siparişlerden biri 1.450 TL'lik, biri çoktan kargoya verilmiş. Bu taslakta ters gidebilecek kaç şey sayabilirsin? Bir de 1. yazıdaki PINEAPPLE yorumunu hatırla: aynı yorum "kullanıcının siparişlerini iptal et" deseydi, bu taslakta onu durduracak bir şey var mı?

Bu yazı 2026 listesinde altıncı sıradan üçüncüye çıkan LLM03 Excessive Agency'yi ele alıyor. Yukarıdaki taslağı adım adım daraltacağız: daha az tool, daha az yetki, daha az otonomi; sonra agent loop'una bir bütçe koyup her kararı bir audit trail'e yazacağız.

Üç Kök Neden: Fonksiyon, Yetki ve Otonomi

OWASP (web uygulama güvenliği üzerine çalışan açık topluluk) Top 10 for LLM (büyük dil modeli) Applications 2026 Excessive Agency'yi şöyle çerçeveliyor: modelin beklenmedik, belirsiz ya da manipüle edilmiş bir çıktısına karşılık zarar verici bir eylemin gerçekleşebilmesi; modelin neden yanlış davrandığından bağımsız olarak. Tetikleyici halüsinasyon da olabilir, direct ya da indirect prompt injection da, çok agent'lı bir sistemde ele geçirilmiş başka bir agent da. Önemli olan şu: tetikleyiciyi kontrol edemiyorsun, sonucunu kontrol edebiliyorsun.

Doküman kök nedeni üçe ayırıyor ve taslağımızda üçü de var. Excessive functionality: tool gerekenden fazlasını yapabiliyor; durum sorgulamak için verilen tool iade de yapabiliyor. Excessive permissions: tool downstream sistemlere gerekenden geniş yetkiyle bağlanıyor; okuma tool'u yazma yetkili bir hesap kullanıyor, kullanıcı adına çalışması gereken tool herkesin verisine erişen bir servis hesabıyla gidiyor. Excessive autonomy: yüksek etkili bir eylem bağımsız bir doğrulama ve onay olmadan gerçekleşiyor. Aşağıdaki bölümler bu üçünü sırayla ele alıyor.

Tool'u Daraltmak: Az Tool, Dar Fonksiyon

manage_orders gibi açık uçlu bir tool, modelin her olası niyetini tek bir kapıdan geçirir. Yerine her biri tek iş yapan ve şeması dar tool'lar yazdım. En önemli karar da şu: modelin elinde bir iptal tool'u yok. Model iptali yalnız hazırlayabiliyor; gerçekleştiren adım modelin dışında:

using System.ComponentModel;
using System.Threading.RateLimiting;

namespace Bookstore.Assistant;

public sealed class AssistantTools(
    OrderStore store, CurrentUser user, PendingCancellations pending, PartitionedRateLimiter<string> previewLimiter)
{
    private const decimal SelfServiceRefundLimit = 1000m;

    [Description("Lists the current customer's orders.")]
    public object GetMyOrders() =>
        store.ForCustomer(user.Id).Select(o => new { o.Id, o.BookTitle, o.Total, o.Status });

    [Description("Prepares the cancellation of one of the current customer's orders and returns a preview. " +
                 "It does not cancel anything; the customer confirms the preview in the app.")]
    public object PreviewCancellation([Description("The order number, for example 1042.")] int orderId)
    {
        // Tool başına, kullanıcı başına limit: saatte en fazla 3 önizleme
        using RateLimitLease lease = previewLimiter.AttemptAcquire(user.Id);
        if (!lease.IsAcquired)
        {
            return new { error = "Too many cancellation requests. Try again later." };
        }

        Order? order = store.Find(orderId, user.Id);
        if (order is null)
        {
            return new { error = $"Order {orderId} was not found for this customer." };
        }
        if (order.Status != "Processing")
        {
            return new { error = $"Order {orderId} is {order.Status} and cannot be cancelled." };
        }

        // Kademeli politika: küçük tutar self-service, büyük tutar insana gider
        if (order.Total > SelfServiceRefundLimit)
        {
            return new { order.Id, order.BookTitle, refund = order.Total, next = "requires_support_review" };
        }

        PendingCancellation p = pending.Create(user.Id, order.Id);
        return new { order.Id, order.BookTitle, refund = order.Total, next = "awaiting_customer_confirmation", p.ConfirmationId };
    }
}

Tool'un içinde dört ayrı karar var ve hiçbiri modele bırakılmamış. Kimin siparişi olduğu CurrentUser'dan, yani oturumdan geliyor; store.Find sahiplik filtresini sorgunun içine koyuyor. Siparişin iptal edilebilir olup olmadığını durum alanı belirliyor. İadenin self-service olup olmadığını bir tutar eşiği belirliyor. Kaç önizleme hazırlanabileceğini bir rate limiter belirliyor. Kimliği oturumdan almak ve tool argümanlarını doğrulamak LLM Temelleri serisinin 12. yazısının konusuydu; burada onların üstüne yetki ve otonomi sınırlarını ekliyoruz.

Excessive functionality'nin daha sessiz bir hâli de var: geliştirme sırasında denenip vazgeçilen ama listeden çıkarılmamış tool'lar. 1. yazıdaki ToolRisk attribute kontrolü gibi bir başlangıç taraması, sınıflandırılmamış her tool'u yakalar; kullanılmayan bir tool'un en güvenli hâli, kodda hiç olmamasıdır.

Yetkiyi Daraltmak: Veritabanında Yalnız SELECT

Tool'un kodu doğru olsa bile, kullandığı kimlik gereğinden güçlüyse bir hata ya da bir injection o gücü kullanabilir. 2026 dokümanının önerisi yetkiyi tool'un kodunda değil, downstream sistemin kendisinde zorlamak. Okuma tool'ları için ayrı, yalnız SELECT yetkisi olan bir veritabanı rolü açtım; aşağıdaki script tabloyu, iki örnek satırı ve assistant_reader rolünü oluşturuyor. Örnek PostgreSQL 17'de; SQL Server'da aynı fikir ayrı bir login, kullanıcı ve GRANT SELECT ile kurulur.

CREATE TABLE orders (
    id          int PRIMARY KEY,
    customer_id text NOT NULL,
    book_title  text NOT NULL,
    total       numeric(10, 2) NOT NULL,
    status      text NOT NULL
);
INSERT INTO orders VALUES (1042, 'ada', 'Refactoring', 650, 'Processing'),
                          (1043, 'grace', 'Clean Architecture', 540, 'Processing');

CREATE ROLE assistant_reader LOGIN;
GRANT SELECT (id, customer_id, book_title, total, status) ON orders TO assistant_reader;

Uygulama okuma tool'ları için bu rolle bağlanıyor. Denemede aynı oturumda SET ROLE ile o role geçip bir okuma, bir güncelleme ve bir silme denedim:

SET ROLE assistant_reader;
SELECT id, status FROM orders WHERE customer_id = 'ada';
UPDATE orders SET status = 'Cancelled' WHERE id = 1043;
DELETE FROM orders;
$ psql -U postgres -d bookstore < as-reader.sql
SET
  id  |   status
------+------------
 1042 | Processing
(1 row)

ERROR:  permission denied for table orders
ERROR:  permission denied for table orders

Okuma çalıştı; UPDATE orders SET status = 'Cancelled' WHERE id = 1043 ve DELETE FROM orders veritabanının kendisi tarafından reddedildi. Tool'da bir SQL injection açığı olsa ya da biri ileride okuma tool'una "küçük bir güncelleme" eklese bile, bu kimlikle yazma yapılamaz. Kolon bazlı GRANT bir adım daha ileri gidiyor: tabloya yarın card_last4 gibi bir kolon eklendiğinde okuma rolü onu kendiliğinden göremez.

Kullanıcı Adına Çalışmak

Excessive permissions'ın ikinci yüzü, kullanıcı adına çalışması gereken bir tool'un herkesin verisine erişen bir servis hesabıyla gitmesi. Bookstore'da sipariş verisi ayrı bir Orders API'sinde duruyorsa, asistanın o API'yi kendi servis kimliğiyle çağırması, tool'un içindeki sahiplik kontrolünü tek savunma hattı yapar. Daha sağlam olanı, downstream çağrıyı kullanıcının kendi token'ıyla yapmak; böylece Orders API kendi authorization kurallarını, sanki kullanıcı onu doğrudan çağırıyormuş gibi uygular:

using System.Net.Http.Headers;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;

namespace Bookstore.Assistant;

// Tool'un çağırdığı Orders API'ye servis hesabıyla değil, kullanıcının kendi token'ıyla gidilir
public sealed class UserAccessTokenHandler(IHttpContextAccessor accessor) : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        HttpContext context = accessor.HttpContext
            ?? throw new InvalidOperationException("Tools that act on behalf of a user need an HTTP request context.");

        string? token = await context.GetTokenAsync("access_token");
        if (string.IsNullOrEmpty(token))
        {
            // Kullanıcı token'ı yoksa servis hesabına düşmek yerine çağrı başarısız olur
            return new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);
        }

        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
        return await base.SendAsync(request, cancellationToken);
    }
}

Bu handler'ı Orders API'nin typed HttpClient'ına ekliyorsun; tool'un kodu değişmiyor. Burada token'ı olduğu gibi iletiyorum; Orders API aynı audience'ı kabul ediyorsa en basit hâli bu. Farklı bir audience ve daha dar bir scope gerekiyorsa doğru yol bir token exchange: on-behalf-of akışıyla ya da RFC 8693 token exchange ile kullanıcının token'ından, yalnız siparişleri okuyabilen kısa ömürlü yeni bir token almak. Access token doğrulama, audience ve scope policy'leri Authentication ve Authorization serisinin 13. yazısının konusuydu. Tek kural şu: kullanıcı token'ı yoksa çağrı başarısız olmalı, sessizce servis hesabına düşmemeli. 2026 dokümanı da çok adımlı ve çok agent'lı akışlarda kullanıcının orijinal yetki kapsamının zincir boyunca korunmasını istiyor; zincirdeki her agent kendi servis kimliğiyle çağrı yaparsa kapsam ilk adımda kaybolur.

Otonomiyi Sınırlamak: Önizleme, Onay ve Tutar Limiti

.NET ile Yapay Zeka serisinin 6. yazısında ApprovalRequiredAIFunction ile bir onay akışı kurmuştuk: model tool'u çağırmak ister, FunctionInvokingChatClient çalıştırmadan durur ve kullanıcıdan onay bekler. Bu yazıda farklı bir yol deniyorum: modelin elinde iptal tool'u hiç yok, yalnız önizleme hazırlayabiliyor. Önizleme kullanıcıya bir kart olarak gösteriliyor; iptali kartın üzerindeki düğme, modelin göremediği ayrı bir endpoint üzerinden yapıyor:

using System.Collections.Concurrent;

namespace Bookstore.Assistant;

public sealed record PendingCancellation(string ConfirmationId, string UserId, int OrderId, DateTimeOffset ExpiresAt);

// Model yalnız önizleme üretir; iptali kullanıcı UI'daki düğmeyle onaylar, model bu adımı göremez
public sealed class PendingCancellations(TimeProvider time)
{
    private readonly ConcurrentDictionary<string, PendingCancellation> _items = new();

    public PendingCancellation Create(string userId, int orderId)
    {
        PendingCancellation pending = new(Guid.NewGuid().ToString("N")[..8], userId, orderId, time.GetUtcNow().AddMinutes(5));
        _items[pending.ConfirmationId] = pending;
        return pending;
    }

    // UI'nın onay kartlarını göstermek için kullandığı liste
    public IReadOnlyList<PendingCancellation> ForUser(string userId) =>
        _items.Values.Where(p => p.UserId == userId).OrderBy(p => p.ExpiresAt).ToList();

    public PendingCancellation? Take(string confirmationId, string userId)
    {
        if (!_items.TryGetValue(confirmationId, out PendingCancellation? pending) || pending.UserId != userId)
        {
            return null;
        }
        _items.TryRemove(confirmationId, out _);
        return pending.ExpiresAt > time.GetUtcNow() ? pending : null;
    }
}
namespace Bookstore.Assistant;

// UI'daki "Onayla" düğmesinin çağırdığı endpoint'in arkasındaki servis; model bu koda erişemez
public sealed class CancellationService(OrderStore store, PendingCancellations pending, AuditLog audit)
{
    public bool Confirm(string confirmationId, CurrentUser user)
    {
        PendingCancellation? p = pending.Take(confirmationId, user.Id);
        if (p is null)
        {
            audit.Write(user.Id, "confirm_cancellation", confirmationId, "denied: unknown, expired or not owned");
            return false;
        }

        store.Cancel(p.OrderId);
        audit.Write(user.Id, "confirm_cancellation", $"order {p.OrderId}", "cancelled", approvedBy: $"{user.Id} (app button)");
        return true;
    }
}

İki yaklaşımın farkı ince ama önemli. ApprovalRequiredAIFunction'da onay, modelin istediği tool çağrısının kendisine veriliyor; onay ekranını iyi tasarlamazsan kullanıcı modelin ürettiği bir argümanı onaylar. Önizleme yaklaşımında onaylanan şey sunucunun kendi verisinden üretilmiş bir kayıt: sipariş numarası, kitap adı ve iade tutarı veritabanından geliyor, modelin cümlesinden değil. 2026 dokümanı da insan onayında özetin değil, gerçekleşecek eylemin tam hâlinin gösterilmesini istiyor. Onay kaydı kullanıcıya bağlı, tek kullanımlık ve beş dakikada sona eriyor. Burada kimlik kısa tutuldu; production'da tam bir Guid kullan.

Tutar eşiği, dokümanın "kademeli politika" dediği şeyin en basit hâli: geri alınabilir ya da düşük etkili eylemler otomatik geçer, geri alınamaz ya da yüksek etkili olanlar bir insana gider. 1.000 TL'nin altındaki iptal müşterinin onayıyla tamamlanıyor; üstündeki destek ekibinin incelemesine düşüyor. Eşiğin değeri bir iş kararı; önemli olan eşiğin kodda olması ve modelin onu değiştirememesi.

Agent Loop'una Bütçe ve Audit Trail

Model "tüm siparişlerimi iptal et" dediğinde kaç tool çağrısı yapacağını sen belirlemiyorsun. Döngüyü üç sınırla çevreledim: MaximumIterationsPerRequest ile model en fazla dört tur tool isteyebiliyor; istek başına toplam tool çağrısı beşle sınırlı; bütün istek beş dakikalık bir duvar saati süresiyle kesiliyor. Toplam çağrı bütçesi için FunctionInvokingChatClient'ın FunctionInvoker property'sini kullanıyorum; her tool çağrısı bu delegate'ten geçiyor, böylece aynı yer audit trail'i de yazıyor:

using System.Text.Json;
using Microsoft.Extensions.AI;

namespace Bookstore.Assistant;

// İstek başına toplam tool çağrısı bütçesi; her çağrı audit log'a düşer
public sealed class ToolBudget(int maxCalls, CurrentUser user, AuditLog audit)
{
    private int _calls;

    public async ValueTask<object?> InvokeAsync(FunctionInvocationContext context, CancellationToken ct)
    {
        string arguments = JsonSerializer.Serialize(context.Arguments, AuditLog.Json);
        if (++_calls > maxCalls)
        {
            context.Terminate = true;
            audit.Write(user.Id, context.Function.Name, arguments, "denied: tool call budget exhausted");
            return new { error = "Tool call budget exhausted for this request." };
        }

        object? result = await context.Function.InvokeAsync(context.Arguments, ct);
        string summary = JsonSerializer.Serialize(result, AuditLog.Json);
        audit.Write(user.Id, context.Function.Name, arguments, "invoked: " + (summary.Length > 90 ? summary[..90] + "..." : summary));
        return result;
    }
}
using System.Text.Encodings.Web;
using System.Text.Json;

namespace Bookstore.Assistant;

public sealed record AuditEntry(DateTimeOffset At, string UserId, string Tool, string Arguments, string Decision, string? ApprovedBy = null);

// Her tool çağrısı ve her onay: kim, hangi tool, hangi argüman, ne karar verildi, kim onayladı
public sealed class AuditLog(TimeProvider time)
{
    public static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web)
    {
        Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
    };

    public void Write(string userId, string tool, string arguments, string decision, string? approvedBy = null)
    {
        AuditEntry entry = new(time.GetUtcNow(), userId, tool, arguments, decision, approvedBy);
        Console.WriteLine("audit " + JsonSerializer.Serialize(entry, Json));
    }
}

Pipeline ve istek; rate limiter kullanıcı başına saatte üç önizlemeye izin veriyor:

PartitionedRateLimiter<string> previewLimiter = PartitionedRateLimiter.Create<string, string>(userId =>
    RateLimitPartition.GetFixedWindowLimiter(userId, _ => new FixedWindowRateLimiterOptions
    {
        PermitLimit = 3,
        Window = TimeSpan.FromHours(1),
        QueueLimit = 0,
    }));

CurrentUser ada = new("ada");
AssistantTools tools = new(store, ada, pending, previewLimiter);
ToolBudget budget = new(maxCalls: 5, ada, audit);

IChatClient ollama = new OllamaApiClient(
    new HttpClient { BaseAddress = new Uri("http://localhost:11434"), Timeout = TimeSpan.FromMinutes(10) }, "qwen3:4b");
IChatClient chat = ollama.AsBuilder()
    .UseFunctionInvocation(configure: f =>
    {
        f.MaximumIterationsPerRequest = 4;     // model en fazla 4 tur tool isteyebilir
        f.FunctionInvoker = budget.InvokeAsync; // istek başına toplam çağrı bütçesi ve audit
    })
    .Build();

ChatOptions options = new()
{
    Tools =
    [
        AIFunctionFactory.Create(tools.GetMyOrders, "get_my_orders"),
        AIFunctionFactory.Create(tools.PreviewCancellation, "preview_cancellation"),
    ],
};

// Tüm agent turu için duvar saati sınırı
using CancellationTokenSource timeout = new(TimeSpan.FromMinutes(5));
ChatResponse response = await chat.GetResponseAsync(
[
    new(ChatRole.System, "You are the Bookstore assistant. You can list the customer's orders and prepare cancellation previews."),
    new(ChatRole.User, message),
], options, timeout.Token);

// Bütçe isteği kestiyse modelin son bir cevap yazacak turu olmaz; kullanıcıya sabit bir mesaj gösterilir
string answer = response.Text.Trim() is { Length: > 0 } text
    ? text.ReplaceLineEndings(" ")
    : "I prepared what I could. Please check the cancellation cards below.";
Console.WriteLine($"answer: {answer}");

// UI: Ada ilk önizlemeyi onaylıyor, grace aynı onay kimliğini deniyor
CancellationService cancellations = new(store, pending, audit);
string? firstConfirmation = pending.ForUser(ada.Id).FirstOrDefault()?.ConfirmationId;
if (firstConfirmation is not null)
{
    cancellations.Confirm(firstConfirmation, new CurrentUser("grace"));
    cancellations.Confirm(firstConfirmation, ada);
    cancellations.Confirm(firstConfirmation, ada);
}
$ dotnet run -- "Please cancel all of my orders."
audit {"at":"2026-11-13T21:59:14.656381+00:00","userId":"ada","tool":"get_my_orders","arguments":"{}","decision":"invoked: [{\"id\":1042,\"bookTitle\":\"Refactoring\",\"total\":650,\"status\":\"Processing\"},{\"id\":1044,\"bookT...","approvedBy":null}
audit {"at":"2026-11-13T22:00:53.623035+00:00","userId":"ada","tool":"preview_cancellation","arguments":"{\"orderId\":1042}","decision":"invoked: {\"id\":1042,\"bookTitle\":\"Refactoring\",\"refund\":650,\"next\":\"awaiting_customer_confirmation\",...","approvedBy":null}
audit {"at":"2026-11-13T22:00:53.623685+00:00","userId":"ada","tool":"preview_cancellation","arguments":"{\"orderId\":1044}","decision":"invoked: {\"id\":1044,\"bookTitle\":\"Clean Code\",\"refund\":480,\"next\":\"awaiting_customer_confirmation\",\"...","approvedBy":null}
audit {"at":"2026-11-13T22:00:53.623971+00:00","userId":"ada","tool":"preview_cancellation","arguments":"{\"orderId\":1045}","decision":"invoked: {\"id\":1045,\"bookTitle\":\"Domain-Driven Design\",\"refund\":1450,\"next\":\"requires_support_revie...","approvedBy":null}
audit {"at":"2026-11-13T22:00:53.624271+00:00","userId":"ada","tool":"preview_cancellation","arguments":"{\"orderId\":1046}","decision":"invoked: {\"error\":\"Too many cancellation requests. Try again later.\"}","approvedBy":null}
audit {"at":"2026-11-13T22:00:53.624837+00:00","userId":"ada","tool":"preview_cancellation","arguments":"{\"orderId\":1047}","decision":"denied: tool call budget exhausted","approvedBy":null}
answer: I prepared what I could. Please check the cancellation cards below.
audit {"at":"2026-11-13T22:00:53.630027+00:00","userId":"grace","tool":"confirm_cancellation","arguments":"b71e2142","decision":"denied: unknown, expired or not owned","approvedBy":null}
audit {"at":"2026-11-13T22:00:53.630169+00:00","userId":"ada","tool":"confirm_cancellation","arguments":"order 1042","decision":"cancelled","approvedBy":"ada (app button)"}
audit {"at":"2026-11-13T22:00:53.630182+00:00","userId":"ada","tool":"confirm_cancellation","arguments":"b71e2142","decision":"denied: unknown, expired or not owned","approvedBy":null}

Audit satırlarını yukarıdan aşağıya okuyalım. qwen3:4b önce siparişleri listeledi, sonra ikinci turda beş önizlemeyi birden istedi. 1042 ve 1044 müşteri onayına hazır. 1045 tutar eşiğini aştığı için destek incelemesine düştü; iptal bile hazırlanmadı. 1046 için rate limiter devreye girdi; saatlik üç önizleme hakkı dolmuştu. Model, kargoya verilmiş olan 1046'yı da denedi; rate limit olmasaydı durum kontrolü reddedecekti. 1047 ise toplam çağrı bütçesine takıldı ve context.Terminate döngüyü bitirdi. Bu yüzden modelin son bir cevap yazacağı tur olmadı; kullanıcıya sabit bir mesaj gösteriliyor ve önizleme kartları UI'da duruyor.

Onay tarafında grace, Ada'nın onay kimliğiyle geldi ve reddedildi; kayıt silinmedi, çünkü Take önce sahipliğe bakıyor. Ada onayladı, 1042 iptal edildi ve audit satırında kimin onayladığı yazıyor. Aynı onayın ikinci kez gönderilmesi reddedildi. Ayrı bir koşuda Ada'nın "Cancel order 1043 please." isteğine model preview_cancellation'ı 1043 ile çağırdı, tool "Order 1043 was not found for this customer." döndü ve cevap aynı cümle oldu; grace'in siparişinin var olduğu bile sızmadı.

Audit trail'in alanları 2026 dokümanının izleme önerisiyle örtüşüyor: kim, hangi tool, hangi argüman, ne karar verildi, kim onayladı. Reddedilen denemeleri de yazmak, başarılı olanları yazmak kadar önemli; yukarıdaki dört "denied" satırı, bir olay incelemesinde neyin denendiğini ve neyin durdurduğunu gösteriyor. Production'da bu satırlar Console'a değil, değiştirilemeyen ve erişimi kısıtlı bir audit hedefine gider.

Agentic Top 10 ile Eşleme

1. yazıda modelin bir bileşenden bir aktöre dönüştüğü anda riskin Agentic Top 10'a, yani ASI (OWASP'ın agentic uygulama güvenliği çalışması) listesine kaydığını söylemiştik. 2026 PDF'inin eşleme tablosu Excessive Agency'yi özellikle iki ASI maddesine bağlıyor. ASI02 Tool Misuse & Exploitation: tool'lar görevin gerektirdiğinden fazla fonksiyon taşıdığında ortaya çıkan durum; bizim manage_orders taslağı ve onu daraltmamız. ASI03 Identity & Privilege Abuse: tool'ların genel ve fazla yetkili kimliklerle downstream'e bağlanması; bizim servis hesabı, assistant_reader rolü ve kullanıcı token'ı. Eşleme tablosu ASI08 Cascading Failures'ı da anıyor; tool çağrılarını durduran rate limit ve bütçe, tam olarak bir hatanın zincirleme büyümesini kesen devre kesiciler.

Bookstore asistanı bugün tek bir agent. Yarın bir "iade agent'ı"na görev devreden çok agent'lı bir yapıya geçerse, ASI07 Insecure Inter-Agent Communication ve kullanıcının yetki kapsamının agent'lar arasında korunması gündeme gelir. MCP (Model Context Protocol; AI modellerini tool'lara ve veri kaynaklarına bağlayan protokol) üzerinden bağlanan tool'larda confused deputy, token passthrough ve sandboxing konularını AI Agent'ları ve MCP serisinin 13. yazısında işlemiştik; buradaki ilkeler o tool'lar için de aynen geçerli.

Üretimde Dikkat

Tool envanterini ve her tool'un kimliğini belgeleyip review et: tool hangi downstream sisteme, hangi kimlikle, hangi yetkiyle gidiyor? Bu liste her sprint'te değişebilir; 1. yazıdaki başlangıç kontrolü yeni tool'ları yakalar ama kimliklerin doğruluğunu insan gözü kontrol etmeli.

Limitleri metriğe bağla. Tool başına çağrı sayısı, rate limit'e takılan istekler, bütçesi biten istekler ve tutar eşiği yüzünden insana giden işlemler birer metrik olmalı. Bütçesi biten istek oranı bir günde birden yükselirse ya modelin davranışı değişti ya da birisi sistemi yokluyor.

Kademeli politikayı ürünle birlikte tasarla. Eşik değerleri, hangi eylemin otomatik geçeceği ve hangisinin insana gideceği bir güvenlik ayarı olduğu kadar bir iş kararıdır. Destek ekibinin inceleme queue'su kapasitesizse, eşik pratikte "her şeyi otomatik onayla"ya döner.

Onay yorgunluğunu hesaba kat. Her küçük işlem için onay isteyen bir sistem, kullanıcıya düşünmeden "Onayla"ya basmayı öğretir. Onayı yalnız geri alınamaz ya da yüksek etkili işlemlere sakla, kartta da işlemin tam hâlini göster.

Sık Düşülen Tuzaklar

Birincisi, açık uçlu tool yazmak. run_sql(query), call_api(url), manage_orders(action, ...) gibi tool'lar modele bir dil veriyor; o dil injection'ın da dili oluyor. Her işi kendi şemasıyla, ayrı bir tool olarak yaz.

İkincisi, servis hesabının yetkisini tool'un koduyla sınırlandırdığını sanmak. Kod bugün doğru olabilir; yetkiyi veritabanında ve downstream API'de zorlamak, yarın yazılacak hatalı koda karşı da korur.

Üçüncüsü, onayı modelin cümlesiyle istemek. "Model 1042'yi iptal etmek istiyor, onaylıyor musun?" ekranında gösterilen bilgi modelin argümanından üretiliyorsa, onaylanan şey modelin iddiası olur. Kartı sunucunun kendi verisinden üret.

Dördüncüsü, yalnız MaximumIterationsPerRequest'e güvenmek. Model tek bir turda beş tool birden isteyebiliyor; yukarıdaki koşu tam olarak bunu yaptı. Tur sınırının yanında toplam çağrı bütçesi ve zaman sınırı da gerekli.

Küçük Bir Deneme

★☆☆ ToolBudget'ın maxCalls değerini 2 yap ve aynı isteği tekrar gönder. Audit trail'de kaç satır görüyorsun, kullanıcıya hangi mesaj gidiyor?

★★☆ Tutar eşiğini tek sipariş yerine kullanıcı başına günlük toplam iade tutarına çevir: aynı gün içinde hazırlanan önizlemelerin toplamı 1.500 TL'yi aşınca yeni önizlemeler destek incelemesine düşsün. Zamanı TimeProvider'dan al ve FakeTimeProvider ile test et.

★★★ UserAccessTokenHandler'ı gerçek bir akışta dene: Orders API'yi ayrı bir minimal API olarak ayağa kaldır, JWT bearer authentication ekle ve asistanın Ada'nın token'ıyla grace'in siparişini isteyince Orders API'den 403 ya da 404 aldığını doğrula. Tool'un içindeki sahiplik filtresini kasıtlı olarak kaldırdığında sonuç değişiyor mu?

Modelin elindeki yetkileri daralttık; geriye modelin ürettiği metnin kendisi kalıyor. Bir sonraki yazıda LLM10 Improper Output Handling ve LLM07 Misinformation'a geçiyoruz: model çıktısını güvenilmeyen girdi gibi ele almak, Angular'da Markdown'u güvenli render etmek, dışarıya veri taşıyan resim linklerini CSP (tarayıcının hangi kaynakları yükleyebileceğini belirleyen güvenlik header'ı) ile kesmek ve structured output'u domain kurallarıyla doğrulamak.