DeepSeek Harness Tool-Ausführungspipeline und Berechtigungsgate

In den vorherigen Kapiteln haben wir gelernt, wie man defineTool verwendet, um Tools zu registrieren, und außerdem, wie man das Event-System nutzt, um verschiedene Erweiterungspunkte abzuhören.

Aber jetzt stellt sich die Frage: Welche Schritte laufen tatsächlich dazwischen ab, nachdem das Modell einen Tool-Aufruf ausgegeben hat, bis das Tool tatsächlich ausgeführt wird und das Ergebnis an das Modell zurückgeht?

In diesem Kapitel erklären wir die "Tool-Ausführungs-Pipeline" von dsh im Detail und wie man Berechtigungs-Gates mit Hook-Plugins implementiert.

In einem Satz: Ein Tool-Aufruf durchläuft mehrere Stufen in einer festen Reihenfolge, jede Stufe ist für eine Kategorie von Richtlinien zuständig, und Hook-Plugins können darin erlauben, verweigern oder nachfragen.


Die Gesamtreihenfolge der Pipeline

Die Tool-Ausführung ist nicht so einfach wie das "Aufrufen einer Funktion"; sie ist eine Pipeline mit einer festen Reihenfolge.

Die offizielle Dokumentation fasst die Reihenfolge so zusammen:tools/pre-execute → Monotoner Guard → tools/execute → tools/post-execute → finalizeContent → tools/result。

Dabei können die ersten drei Waterfalls einen Aufruf einmal umschreiben, während finalizeContent und tools/result, die von der Definition selbst gesteuert werden, danach ausgeführt werden.

工具执行流水线流程图

Betrachten wir zunächst zwei Konzepte, die leicht verwechselt werden:

waterfall(Waterfall-Ereignis) ist ein Ereignis-Dispatch-Muster; Listener könnennext()die Entscheidungsbefugnis weiterreichen oder direkt eine Entscheidung zurückgeben, um kurzzuschließen.

Monotoner Guard(Monotoner Guard) ist eine letzte Verteidigungslinie, die nur Verschärfung zulässt, niemals Rücknahme.

Im Folgenden erklären wir jeden Schritt einzeln.


tools/pre-execute: eine umsortierbare Richtlinienebene

tools/pre-executeEs ist der erste Waterfall in der Pipeline und dafür verantwortlich, umsortierbare Richtlinien wie "Hooks, Berechtigungen, Sandbox." zu tragen.

Der Grund, warum es "reorderable" genannt wird, ist, dass Listenernext()die Entscheidungsbefugnis an den nächsten Listener weitergeben können, und die Reihenfolge mehrerer Policy-Plugins kann in der Konfiguration angepasst werden.

Dieser Waterfall gibt eine typisierte Entscheidung zurückPreToolDecisionEs gibt drei mögliche Werte:

EntscheidungBedeutungNachfolgendes Verhalten
{ kind: 'allow' }Diesen Aufruf zulassenMit dem monotonen Guard und den nachfolgenden Schritten fortfahren.
{ kind: 'deny'; reason: string }Diesen Aufruf ablehnenEs wird in ein Fehlerergebnis materialisiert, und der Tool-Body wird übersprungen.
{ kind: 'ask'; reason?: string }Den Benutzer fragenNur wenn der Genehmigungsdienst zurückgibtallowed-onceDann fortfahren, andernfalls ablehnen.

HinweisaskBranch: Er wird ausgelöstctx.approvalals einmalige Abfrage; dieser Teil wird für den nächsten Artikel aufgehoben.

Parameter können nicht geändert werden, weil Verlaufsaufzeichnungen, Audit, UI und Ausführung konsistent bleiben müssen.

Wann sollte pre-execute verwendet werden?

Wenn eine Richtlinie eine der drei Aktionen — "allow, deny oder ask" — benötigt und Richtlinien frei sortierbar sein sollen, verwenden Sie es.

Plugins wie sandbox, permission und plan-mode verwenden alle diesen Erweiterungspunkt.


Monotoner Guard: eine unwiderrufliche endgültige Ablehnung.

Der Nachteil von Waterfall ist, dass später registrierte Listener die Entscheidungen früherer Listener umstoßen können.

Wenn eine Invariante "endgültige Ablehnung, und niemand kann sie widerrufen," erfordert, verwenden Siectx.tools.guard()。

Der Typ des Guards istToolGuardEr hat absichtlich kein allow-Ergebnis: Die Rückgabe einer Zeichenkette zeigt Ablehnung an, die RückgabeundefinedZeigt die Beibehaltung des Status quo an.

// ToolGuard:感知作用域的最终预分派策略
type ToolGuard = (execution: Readonly<ToolExecution>) => string | undefined

Da der Guard kein allow-Ergebnis hat, kann die Reihenfolge der Listener eine Ablehnung niemals wieder in eine Genehmigung umwandeln.

Das bedeutet "monoton": nur verringern, niemals erhöhen; nur Berechtigungen entziehen, niemals erteilen.

Praktische Grundsätze: Platzieren Sie hier umsortierbare Richtlinien.tools/pre-execute; Invarianten, die "endgültig und unwiderruflich" sein müssen, gehören inctx.tools.guard()。


tools/execute und tools/post-execute

tools/executeEs ist für den "Around-Dispatch" verantwortlich, das heißt für das Umhüllen des tatsächlichen Aufrufs des Tool-Bodys.

Timeout, Wiederholung und Metrikerfassung werden alle auf dieser Ebene behandelt; was sie umschließt, ist der tatsächliche Dispatch-Lebenszyklus.

Die Ansicht, die es erhält, istToolDispatchExecution: Nur diese Ansicht kann die erforderliche ersetzenexec.signal, verwendet, um eine Deadline zu setzen.

Die Ersetzungsregel lautet: Sie kann ersetzt, aber nicht entfernt werden; die Registry wird das Signal des Aufrufers vor dem Aufruf des Tool-Bodys erneut zusammenführen.

tools/post-executeEs ist für die Prüfung oder Neuschreibung nach der Tool-Ausführung und vor der Ergebnisnormalisierung verantwortlich; es gibtPostToolDecision zurück:

EntscheidungBedeutung
{ kind: 'accept'; content? }Ergebnis akzeptieren, kann den angezeigten Inhalt ersetzen (kanonischen Wert und Metadaten beibehaltend).
{ kind: 'accept'; value }Ergebnis akzeptieren, kann den kanonischen Wert ersetzen (wird den Inhalt erneut validieren und neu berechnen).
{ kind: 'block'; feedback }Blockiere das Ergebnis, wodurch korrigierendes Feedback in ein Fehlerergebnis umgewandelt wird.

Inhaltsersetzung ist eine Anzeigerichtlinie, keine Vertraulichkeitsrichtlinie.

Um einen programmatischen Wert zu verbergen, müssen Sie diesen Wert ersetzen oder das Ergebnis blockieren.


finalizeContent und tools/result

finalizeContentEs ist ein Callback, der sich im Besitz der ToolDefinition selbst befindet, und die Registry ruft ihn genau einmal auf.

Es ist die "letzte rein inhaltliche Invariante": synchron ausgeführt, wobei nur abschließende Korrekturen auf Inhaltsebene erlaubt sind.

Danach materialisiert die Registry das akzeptierte Ergebnis und friert es ein, dann löst sie austools/result。

tools/resultEs ist eine synchrone Benachrichtigung, die zur Beobachtung dient.Eingefrorenes, unveränderliches, maßgebliches Ergebnis。

Beobachter können das Ergebnis nicht transformieren, und Beobachterfehler sind isoliert und wirken sich nicht auf den Hauptablauf aus.

Verwenden Sie es, wenn Sie "Audit, Metriken, Erfassung des endgültigen Ergebnisses" benötigen; verwenden Sie es nur, wenn Sie "das Ergebnis transformieren oder Kontext anhängen" müssen.tools/post-execute。

Merkhilfe für die Auswahl: pre-execute entscheidet, "ob es durchgeführt werden kann", execute entscheidet, "wie es ausgeführt wird", post-execute entscheidet, "wie das Ergebnis präsentiert wird", result ist nur für "einen Blick auf das Endergebnis" verantwortlich.


Praxisbeispiel: Schreiben Sie ein Berechtigungs-Gate-Plugin.

Die offizielle Dokumentation verwendet "Permission Gate" als Beispiel, um zu zeigen, wie Hook-Plugins es verwenden.tools/pre-execute。

Hook-Plugins sind gewöhnliche Cordis-Plugins und erfordern kein externes Protokoll.

Beispiel

// Dateipfad: my-plugins/permission-gate/src/index.ts
// Ein Permission-Gate-Plugin auf Basis von tools/pre-execute.
// Es gibt eine typisierte Entscheidung zurück: ablehnen, wenn auf der Blacklist, andernfalls next() aufrufen, um zu delegieren.
import type { Context } from '@deepseek-ai/cordis'
import type { PreToolDecision, ToolExecution } from '@deepseek-ai/dsh-tools'

// Blacklist: Tools, denen es im Beispielprojekt verboten ist, direkt in das Dateisystem zu schreiben.
// Hier verwenden wir zur Demonstration ein einfaches Set; in echten Projekten können Sie eine Datenbank abfragen oder einen Genehmigungsdienst konsultieren.
const DENY_TOOLS = new Set(['fs_write', 'fs_edit'])

// Richtlinien-Entscheidungsfunktion: gibt zurück, ob dieser Aufruf erlaubt ist.
// exec trägt eine unveränderliche Aufrufidentität (callId, name, arguments, agent, token, signal).
async function isAllowed(exec: ToolExecution): Promise<boolesch> {
  if (DENY_TOOLS.has(exec.name)) return false
  // Zusätzliches Beispiel: In der Beispiel-Demo ist das Ändern von .env-Dateien verboten (Argumente werden eingefroren, bevor sie in die Richtlinie gelangen).
  const raw = exec.arguments as { path?: string }
  if (typeof raw.path === 'string' && raw.path.includes('.env')) return false
  return true
}

export const name = 'permission-gate'

export function apply(ctx: Context) {
  // tools/pre-execute ist ein Waterfall: Listener können eine Entscheidung zurückgeben oder next() aufrufen, um zu delegieren.
  ctx.on('tools/pre-execute', async (exec, next): Promise<PreToolDecision> => {
    if (!(await isAllowed(exec))) {
      // Das Zurückgeben von deny beendet diesen Aufruf sofort; nachfolgende Listener werden nicht ausgeführt.
      return { kind: 'deny', reason: 'Von der Richtlinie abgelehnt: Dieses Tool ist im Beispiel-Workspace nicht erlaubt.' }
    }
    // durchreichen: Die Entscheidung an nachfolgende Listener in der Pipeline übergeben.
    return next()
  })
}

Nachdem dieses Plugin geladen wurde, durchläuft jeder vom Modell ausgeführte Tool-Aufruf zuerst dieses Plugin.

Ein Tool, das in die Blacklist fällt, erhält ein Fehlerergebnis mit einer Begründung, und der Tool-Body wird übersprungen.

Da es eine typisierte Entscheidung zurückgibt, können andere Policy-Plugins danach weiterhin an der Entscheidung teilnehmen.


Praxisbeispiel: Hinzufügen eines Timeouts mit tools/execute

Timeout gehört zum "around dispatch"-Aspekt und sollte umhüllt werden mittools/execute, anstatt in den Tool-Body geschrieben zu werden.

Beispiel

// Dateipfad: my-plugins/tool-guard/src/index.ts
// Verwende tools/execute, um den tatsächlichen Dispatch-Lebenszyklus zu umhüllen: Füge jedem Tool-Aufruf ein 30-Sekunden-Timeout hinzu.
import type { Context } from '@deepseek-ai/cordis'

export const name = 'tool-guard'

export function apply(ctx: Context) {
  // tools/execute ist ein Waterfall: next() muss aufgerufen werden, um den eigentlichen Tool-Body auszuführen.
  ctx.on('tools/execute', async (exec, next) => {
    // Hinweis: Nur die tools/execute-Ansicht kann exec.signal ersetzen.
    // Hier wird das Signal des Aufrufers mit einem 30-Sekunden-Timeout kombiniert, wodurch das ursprüngliche Signal überschrieben wird.
    const originalSignal = exec.signal
    const deadline = AbortSignal.timeout(30_000)
    exec.signal = AbortSignal.any([originalSignal, deadline])
    try {
      // An die eigentliche Tool-Ausführung execute() delegieren; Timeout oder Abbruch durch den Aufrufer lösen abort aus.
      return await next()
    } finally {
      // Das ursprüngliche Signal nach der Verwendung wiederherstellen, um nachfolgende Aufrufe nicht zu verunreinigen.
      exec.signal = originalSignal
    }
  })
}

Regel: exec.signal kann ersetzt werden, aber nicht entfernt werden.

Die Registry wird das Signal des Aufrufers erneut zusammenführen, bevor sie den Funktionskörper des Tools aufruft, daher ist es für die Wrapper-Schicht sicher, das Signal zu ersetzen.


Praxisbeispiel: monotone Ablehnung mit ctx.tools.guard()

Wenn eine Invariante "endgültige Ablehnung, unwiderruflich" erfordert, verwenden Siectx.tools.guard()。

Beachten Sie, dass der Rückgabetyp des Guardsstring | undefined, es gibt keinen Allow-Zweig.

Beispiel

// Dateipfad: my-plugins/invariant-guard/src/index.ts
// Eine unwiderrufliche endgültige Ablehnung: verhindert die Ausführung von run_code in der Beispiel-Demo-Umgebung.
import type { Context } from '@deepseek-ai/cordis'

export const name = 'invariant-guard'

export function apply(ctx: Context) {
  // Einen monotonen Guard registrieren. Die Rückgabe eines Strings bedeutet Ablehnung; die Rückgabe von undefined bedeutet keine Änderung.
  const disposer = ctx.tools.guard((execution) => {
    if (execution.name === 'run_code') {
      return 'run_code ist im Beispiel-Demo-Profil deaktiviert.'
    }
    return undefined // Durchreichen: die Pre-Execute-Entscheidung unverändert lassen
  })

  // guard() gibt eine präzise Cleanup-Funktion zurück; das Framework ruft sie automatisch auf, wenn das Plugin entladen wird.
  ctx.effect(() => disposer)
}

Da der Guard kein Allow-Ergebnis hat, kann selbst ein späterer Listener, der erlauben möchterun_code, diese Ablehnung nicht wieder in eine Genehmigung umwandeln.


Zusammenfassung Selbsttest

Die Tool-Ausführungspipeline entkoppelt „Richtlinien“ vom „Tool-Body“, sodass Hooks verschiedene Tool-Familien abdecken können, ohne Tools an einen bestimmten Policy-Dienst zu koppeln.

Selbsttestfragen:

ProblemReferenz
Wenn ein Tool von niemandem jemals genehmigt werden können soll, solltest du dann pre-execute oder guard verwenden?guard, weil guard kein Allow-Ergebnis hat und unwiderruflich ist.
Um allen Tools ein einheitliches Timeout hinzuzufügen, auf welches Event solltest du hören?tools/execute, es umschließt dispatch und kann signal ersetzen
Wenn du nur das endgültige Ergebnis für Auditing aufzeichnen möchtest, ohne etwas zu ändern, welchen Schritt solltest du verwenden?tools/resultEs beobachtet das eingefrorene autoritative Ergebnis
Weitere Erweiterungen