Einführung
Auch ein validiertes Tool kann zu schnell handeln. Wenn ein Modell eine Änderung an einem Datensatz vorschlägt, muss möglicherweise eine Person die genaue Aktion prüfen, bevor irgendein Schreibvorgang stattfindet. Eine menschliche Genehmigung im Ablauf hält den Tool-Aufruf an, zeigt seine Argumente im Client an und ermöglicht einer Person, ihn zu genehmigen oder abzulehnen.
In diesem Lab erweitern Sie einen kleinen synthetischen Support-Agent um ein Lesetool und eine genehmigungspflichtige Aktualisierung:
lookupSupportCasebleibt ein schreibgeschütztes Server-Tool und läuft ohne Genehmigung.requestPriorityChangeverwendet die unterstützte OptionneedsApproval. Daher kann seine Funktionexecuteerst ausgeführt werden, wenn der Client eine Genehmigungsantwort sendet.- Der React-Client zeigt die ausstehende Aktion an und ruft für Approve oder Deny
addToolApprovalResponse()auf. - Ein dauerhaftes Idempotenzprotokoll speichert den Operationsschlüssel. Bei einer wiederholten genehmigten Zustellung wird deshalb das erste Ergebnis zurückgegeben, statt eine zweite Aktualisierung anzuwenden.
- Deterministische Prüfungen und ein begrenzter Workers-AI-Ablauf weisen die Zustände „ausstehend“, „abgelehnt“, „genehmigt“ und „Duplikat“ nach.
Genehmigung und Autorisierung beantworten unterschiedliche Fragen. Die Autorisierung begrenzt, welche Warteschlange die signierte Sitzung erreichen darf. Die Genehmigung klärt, ob eine Person genau diese vorgeschlagene Änderung akzeptiert. Die Idempotenz behandelt ein drittes Problem: Ein Netzwerk oder Client kann dieselbe genehmigte Aktion mehr als einmal zustellen. Alle Datensätze in diesem Lab sind synthetisch und nicht dauerhaft; es ist kein echtes Helpdesk-System verbunden.
Die bereitgestellte Shell und das kurzlebige Sitzungstoken halten den Fokus auf der Genehmigungsgrenze und nicht auf Frontend- oder Authentifizierungsgrundlagen. Kostenlose Workers-AI-Kontingente werden mit anderen Aktivitäten im Konto geteilt. Wenn kein Kontingent mehr verfügbar ist, brechen Sie ab, statt einen kostenpflichtigen Tarif zu aktivieren.
Bevor Sie diesen Kurs direkt beginnen, absolvieren Sie LabEx mit Ihrem Cloudflare-Konto verbinden. Jede neue LabEx-VM benötigt eine eigene Wrangler-Autorisierung. Frühere Labs des Kurses werden empfohlen, aber ihre VMs und Ressourcen werden hier niemals wiederverwendet.
VM autorisieren und den Genehmigungs-Worker deklarieren
In diesem Schritt autorisieren Sie die neue VM und deklarieren die Ressourcen, die der genehmigungspflichtige Agent verwendet.
Öffnen Sie ein Terminal und wechseln Sie in das vorbereitete Projekt:
cd /home/labex/project/approval-record-changes
Autorisieren Sie diese VM:
npx wrangler login
Öffnen Sie den angezeigten Link, genehmigen Sie die dokumentierten Wrangler-Berechtigungen für Ihr dediziertes Lernkonto und kehren Sie anschließend zum Terminal zurück. Prüfen Sie das strukturierte Ergebnis:
npx wrangler whoami --json
Suchen Sie nach "loggedIn": true, bestätigen Sie den Kontonamen und kopieren Sie die tatsächliche ID dieses Kontos. Speichern Sie sie zusammen mit einem eindeutigen, nicht dauerhaften Worker-Namen:
ACCOUNT_ID="paste-your-confirmed-account-id"
RUN="labex-c11-s06-$(openssl rand -hex 6)"
cat > wrangler.jsonc <<JSON
{
"\$schema": "./node_modules/wrangler/config-schema.json",
"name": "$RUN",
"account_id": "$ACCOUNT_ID",
"main": "src/server.ts",
"compatibility_date": "2026-09-18",
"compatibility_flags": ["nodejs_compat"],
"workers_dev": true,
"preview_urls": false,
"observability": { "enabled": true },
"ai": { "binding": "AI", "remote": true },
"durable_objects": {
"bindings": [
{ "name": "ApprovalAgent", "class_name": "ApprovalAgent" }
]
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["ApprovalAgent"] }
]
}
JSON
python3 .labex/verify.py authorization
Das Binding AI stellt die Modellausführung bereit, ohne einen API-Schlüssel einzubetten. Das Durable-Object-Binding gibt jedem benannten ApprovalAgent einen eigenen SQLite-Speicher für den synthetischen Fall und sein Idempotenzprotokoll. Der Browser verwendet den Namen planning; ein anderer Name erhält eine separate Instanz und kann die Daten von planning nicht sehen. Es wurde noch nichts bereitgestellt.
Tool-Verträge definieren
In diesem Schritt legen Sie genau fest, welche Argumente jedes Tool akzeptiert.
Ein Tool-Schema ist ein Laufzeitvertrag. TypeScript-Typen helfen beim Kompilieren, aber die Modellausgabe kommt zur Laufzeit an und muss erneut geprüft werden. Erstellen Sie src/cases.ts:
cat > src/cases.ts <<'TS'
import { z } from "zod";
const queue = z.string()
.min(3)
.max(40)
.regex(/^[a-z0-9-]+$/, "queue must use lowercase letters, digits or hyphens");
export const lookupCaseInput = z.object({
queue,
ticketId: z.literal("T-SYNTH-101")
}).strict();
export const changePriorityInput = lookupCaseInput.extend({
priority: z.literal("high"),
operationKey: z.string()
.min(8)
.max(80)
.regex(/^[a-z0-9-]+$/, "operation key must use lowercase letters, digits or hyphens")
}).strict();
export type LookupCaseInput = z.infer<typeof lookupCaseInput>;
export type ChangePriorityInput = z.infer<typeof changePriorityInput>;
export type SupportCase = {
queue: string;
ticketId: "T-SYNTH-101";
summary: string;
priority: "low" | "medium" | "high";
revision: number;
};
export type ChangeResult = SupportCase & { duplicate: boolean; operationKey: string };
export function parseInput<T>(schema: z.ZodType<T>, input: unknown): T {
const result = schema.safeParse(input);
if (!result.success) {
const issue = result.error.issues[0];
throw new Error(`invalid tool input: ${issue.path.join(".") || "request"} ${issue.message}`);
}
return result.data;
}
TS
python3 .labex/verify.py schemas
Der Lesevertrag akzeptiert nur einen gültigen Warteschlangennamen und das eine synthetische Ticket. Der Änderungsvertrag beschränkt die Übung auf die Priorität high und verlangt einen stabilen operationKey. .strict() weist außerdem unerwartete Felder zurück. Dadurch werden Mehrdeutigkeiten reduziert und Aufrufer daran gehindert, nicht unterstützte Anweisungen in die Operation einzuschleusen.
Ein Idempotenzschlüssel identifiziert eine logische Operation über mehrere Wiederholungsversuche hinweg. Der Server speichert das erste erfolgreiche Ergebnis unter diesem Schlüssel. Wenn dieselbe genehmigte Operation erneut eingeht, gibt er das gespeicherte Ergebnis zurück, statt zweimal zu schreiben. Die Validierung erteilt weder Genehmigung noch Zugriff: Der Agent prüft weiterhin seinen dauerhaften Namen, und das SDK wartet weiterhin auf die Entscheidung der Person.
Genehmigungssperre und idempotenten Effekt implementieren
In diesem Schritt lassen Sie Lesevorgänge automatisch zu, halten den Schreibvorgang mit needsApproval an und machen den genehmigten Effekt idempotent.
Erstellen Sie src/server.ts:
cat > src/server.ts <<'TS'
import { AIChatAgent, type OnChatMessageOptions } from "@cloudflare/ai-chat";
import { callable, routeAgentRequest } from "agents";
import { convertToModelMessages, stepCountIs, streamText, tool } from "ai";
import { createWorkersAI } from "workers-ai-provider";
import {
changePriorityInput,
lookupCaseInput,
parseInput,
type ChangePriorityInput,
type ChangeResult,
type LookupCaseInput,
type SupportCase
} from "./cases";
import { verifySessionRequest } from "./session-auth";
export class ApprovalAgent extends AIChatAgent<Cloudflare.Env> {
maxPersistedMessages = 12;
private ensureTables(): void {
this.sql`CREATE TABLE IF NOT EXISTS support_cases (
ticket_id TEXT PRIMARY KEY,
queue TEXT NOT NULL,
case_summary TEXT NOT NULL,
priority TEXT NOT NULL,
revision INTEGER NOT NULL
)`;
this.sql`INSERT OR IGNORE INTO support_cases
(ticket_id, queue, case_summary, priority, revision)
VALUES ('T-SYNTH-101', ${this.name}, 'Synthetic customer cannot open a sample invoice', 'medium', 0)`;
this.sql`CREATE TABLE IF NOT EXISTS approval_operations (
operation_key TEXT PRIMARY KEY,
ticket_id TEXT NOT NULL,
applied_revision INTEGER NOT NULL
)`;
}
private scopedCase(input: LookupCaseInput): SupportCase {
if (input.queue !== this.name) throw new Error("queue is outside this Agent scope");
this.ensureTables();
const rows = this.sql<{
queue: string;
ticketId: "T-SYNTH-101";
summary: string;
priority: "low" | "medium" | "high";
revision: number;
}>`SELECT queue, ticket_id AS ticketId, case_summary AS summary, priority, revision
FROM support_cases WHERE ticket_id = ${input.ticketId}`;
const record = rows[0];
if (!record || record.queue !== this.name) throw new Error("case not found in this Agent scope");
return record;
}
@callable()
inspectCase(input: unknown): SupportCase {
return this.scopedCase(parseInput(lookupCaseInput, input));
}
private applyApprovedChange(input: unknown): ChangeResult {
const parsed: ChangePriorityInput = parseInput(changePriorityInput, input);
const current = this.scopedCase(parsed);
const prior = this.sql<{ appliedRevision: number }>`SELECT applied_revision AS appliedRevision
FROM approval_operations WHERE operation_key = ${parsed.operationKey}`[0];
if (prior) {
return { ...current, duplicate: true, operationKey: parsed.operationKey };
}
this.sql`UPDATE support_cases
SET priority = ${parsed.priority}, revision = ${current.revision + 1}
WHERE ticket_id = ${parsed.ticketId} AND queue = ${this.name}`;
const changed = this.scopedCase(parsed);
this.sql`INSERT INTO approval_operations (operation_key, ticket_id, applied_revision)
VALUES (${parsed.operationKey}, ${parsed.ticketId}, ${changed.revision})`;
console.log(JSON.stringify({
event: "approval_change_applied",
instance: this.name,
operationKey: parsed.operationKey,
revision: changed.revision
}));
return { ...changed, duplicate: false, operationKey: parsed.operationKey };
}
@callable()
async verifyApprovedChange(input: unknown, proof: string): Promise<ChangeResult> {
const parsed = parseInput(changePriorityInput, input);
const payload = new TextEncoder().encode(`approval-probe:${JSON.stringify(parsed)}`);
const key = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(this.env.SESSION_SIGNING_KEY),
{ name: "HMAC", hash: "SHA-256" },
false,
["verify"]
);
const normalized = proof.replace(/-/g, "+").replace(/_/g, "/");
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
const signature = Uint8Array.from(atob(padded), (character) => character.charCodeAt(0));
const valid = await crypto.subtle.verify("HMAC", key, signature, payload);
if (!valid) throw new Error("approval probe proof is invalid");
return this.applyApprovedChange(parsed);
}
async onChatMessage(_onFinish: unknown, options?: OnChatMessageOptions) {
const tools = {
lookupSupportCase: tool({
description: "Read synthetic ticket T-SYNTH-101 only from the current named support queue.",
inputSchema: lookupCaseInput,
execute: async (input) => this.inspectCase(input)
}),
requestPriorityChange: tool({
description: "Set synthetic ticket T-SYNTH-101 to high priority in the current queue. Use operation key raise-synthetic-priority.",
inputSchema: changePriorityInput,
needsApproval: true,
execute: async (input) => this.applyApprovedChange(input)
})
};
const workersai = createWorkersAI({ binding: this.env.AI });
const result = streamText({
model: workersai("@cf/zai-org/glm-4.7-flash", {
reasoning_effort: null,
chat_template_kwargs: { enable_thinking: false }
}),
system: `You assist only the synthetic ${this.name} queue. Perform exactly the one action the user requests. For a lookup, call lookupSupportCase only. For a priority request, call requestPriorityChange only with operationKey raise-synthetic-priority and wait for the human decision. Never claim a denied or pending change happened. Keep the final answer to one short sentence.`,
messages: await convertToModelMessages(this.messages),
tools,
stopWhen: stepCountIs(4),
maxOutputTokens: 96,
temperature: 0,
abortSignal: options?.abortSignal
});
return result.toUIMessageStreamResponse();
}
}
export default {
async fetch(request: Request, env: Cloudflare.Env): Promise<Response> {
const authorize = (candidate: Request, route: { name: string }) =>
verifySessionRequest(candidate, route.name, env.SESSION_SIGNING_KEY);
return (await routeAgentRequest(request, env, {
onBeforeConnect: authorize,
onBeforeRequest: authorize
})) ?? new Response("Not found", { status: 404 });
}
};
TS
python3 .labex/verify.py server
Das Modell erhält niemals direkten Datenbankzugriff. Es schlägt typisierte Argumente vor, aber needsApproval: true verhindert, dass execute ausgeführt wird, bevor der Client eine positive Genehmigungsantwort übermittelt. Eine Ablehnung lässt die Methode daher unverändert. Das schreibgeschützte Callable unterstützt eine deterministische Prüfung. Ein separates Prüf-Callable kann den Effekt nur mit einem HMAC-Nachweis auslösen, der aus dem lokalen Signiergeheimnis abgeleitet wird. Ein gewöhnlicher Browser-Client kann die menschliche Genehmigungssperre daher nicht umgehen.
Der Agent verarbeitet jeden synchronen Callable-Aufruf ohne await, sodass eine zweite Zustellung die von der ersten Zustellung erzeugte Protokollzeile vorfindet. Der stabile Operationsschlüssel gibt das bereits angewendete Ergebnis mit duplicate: true zurück; der Datensatz wird nicht erneut erhöht. Protokolliert werden nur das Ereignis, die Agent-Instanz, der Operationsschlüssel und die Revision – nicht der Falltext.
Menschliche Genehmigungsentscheidung anzeigen
In diesem Schritt zeigen Sie einen ausstehenden Tool-Aufruf als ausdrückliche Entscheidung an, statt ihn unbemerkt auszuführen.
Erstellen Sie die TypeScript- und Vite-Konfiguration:
cat > tsconfig.json <<'JSON'
{
"extends": "agents/tsconfig",
"compilerOptions": {
"jsx": "react-jsx",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["@cloudflare/workers-types", "vite/client", "node"]
},
"include": ["src/**/*.ts", "src/**/*.tsx", "vite.config.ts", "worker-configuration.d.ts"]
}
JSON
cat > vite.config.ts <<'TS'
import { cloudflare } from "@cloudflare/vite-plugin";
import react from "@vitejs/plugin-react";
import agents from "agents/vite";
import { defineConfig } from "vite";
export default defineConfig({ plugins: [react(), agents(), cloudflare()] });
TS
Erstellen Sie src/client.tsx:
cat > src/client.tsx <<'TSX'
import { getToolApproval, useAgentChat } from "@cloudflare/ai-chat/react";
import { useAgent } from "agents/react";
import { getToolName, isToolUIPart } from "ai";
import { Suspense } from "react";
import { createRoot } from "react-dom/client";
function ApprovalChat() {
const parameters = new URLSearchParams(window.location.search);
const session = parameters.get("session") ?? "";
const token = parameters.get("token") ?? "";
if (!session || !token) {
return <main><h1>Signed session required</h1><p className="help">Open the complete URL printed by the token command.</p></main>;
}
const agent = useAgent({
agent: "ApprovalAgent",
name: session,
host: window.location.host,
query: { token }
});
const { messages, sendMessage, addToolApprovalResponse, status, error } = useAgentChat({
agent,
autoContinueAfterToolResult: false
});
return (
<main>
<p className="eyebrow">Human approval before effect</p>
<h1>Synthetic Change Review</h1>
<p className="scope">Allowed queue: <strong>{session}</strong> · allowed ticket: <strong>T-SYNTH-101</strong></p>
<p className="status">Status: <strong>{status}</strong></p>
<section className="messages" aria-live="polite">
{messages.length === 0 && <p className="empty">No change request in this signed session yet.</p>}
{messages.map((message) => (
<article className={`message ${message.role}`} key={message.id}>
<span className="role">{message.role}</span>
{message.parts.map((part, index) => {
if (part.type === "text") return <span key={index}>{part.text}</span>;
if (isToolUIPart(part)) {
const toolName = getToolName(part);
if ("approval" in part && part.state === "approval-requested") {
const approvalId = getToolApproval(part)?.id;
return (
<div className="approval" key={part.toolCallId}>
<strong>Approval required: {toolName}</strong>
<p>Review these exact synthetic arguments. No record has changed yet.</p>
<pre>{JSON.stringify(part.input, null, 2)}</pre>
<div className="approval-actions">
<button disabled={!approvalId} () => {
if (!approvalId) return;
await addToolApprovalResponse({ id: approvalId, approved: true });
sendMessage();
}}>Approve</button>
<button className="deny" disabled={!approvalId} => approvalId && addToolApprovalResponse({ id: approvalId, approved: false })}>Deny</button>
</div>
</div>
);
}
return (
<div className="tool-card" key={part.toolCallId}>
<strong>{toolName}</strong><span className="tool">{part.state}</span>
{"output" in part && part.output !== undefined && <pre>{JSON.stringify(part.output, null, 2)}</pre>}
</div>
);
}
return null;
})}
</article>
))}
</section>
<div className="quick-actions">
<button type="button" disabled={status === "streaming" || status === "submitted"} => sendMessage({ text: `Look up T-SYNTH-101 in ${session}.` })}>Check current case</button>
<span>Read-only: safe before and after a decision.</span>
</div>
<form => {
event.preventDefault();
const input = event.currentTarget.elements.namedItem("message") as HTMLInputElement;
const text = input.value.trim();
if (!text) return;
sendMessage({ text });
}}>
<input name="message" defaultValue={`Request high priority for T-SYNTH-101 in ${session} with operation key raise-synthetic-priority.`} maxLength={220} aria-label="Change request" />
<button type="submit" disabled={status === "streaming" || status === "submitted"}>Send</button>
</form>
<p className="notice">Training fixture only: this page cannot reach a real support system.</p>
{error && <p className="error" role="alert">{error.message}</p>}
</main>
);
}
createRoot(document.getElementById("root")!).render(
<Suspense fallback={<main><p>Restoring the signed approval session…</p></main>}><ApprovalChat /></Suspense>
);
TSX
python3 .labex/verify.py client
useAgent() stellt genau zu einem benannten Agent mit seinem kurzlebigen Token eine Verbindung her. Die schreibgeschützte Schaltfläche und das Änderungsformular erzeugen bewusst getrennte Durchläufe: Jeder Durchlauf hat nur einen Zweck. So können Sie den Zustand vor und nach einer Entscheidung beobachten, ohne ein bereits abgeschlossenes Lesetool mit dem angehaltenen Schreibvorgang zu vermischen. useAgentChat() stellt die Hilfsfunktion für die Genehmigungsantwort bereit. Der Agents-Client benachrichtigt den Server über diese Entscheidung. Die automatische Fortsetzung des Clients ist hier deaktiviert, damit derselbe genehmigte Tool-Aufruf nicht ein zweites Mal übermittelt wird. Das Tool-Ergebnis und eine neue Leseabfrage liefern einen zuverlässigeren Nachweis als ein zusätzlicher, vom Modell erzeugter zusammenfassender Satz. isToolUIPart() unterscheidet eine Tool-Aktion von gewöhnlichem Assistententext, während getToolApproval() das Genehmigungsobjekt über die vom SDK unterstützte Schnittstelle liest. Die Genehmigungs-ID bindet die Entscheidung der Person an genau diesen Tool-Aufruf; der Browser ruft die Datenbankmethode nicht direkt auf. Die JSON-Karten machen das Abfrageergebnis, die vorgeschlagenen Argumente und das spätere Schreibergebnis sichtbar, ohne Kontozugangsdaten offenzulegen.
Anwendung lokal erstellen und Grenzen nachweisen
In diesem Schritt kompilieren Sie die Anwendung und testen die tatsächliche Tool-Implementierung, ohne einen Modellaufruf zu verbrauchen.
Erzeugen Sie die exakten Umgebungstypen, prüfen Sie die Typen und erstellen Sie beide Bundles:
npx wrangler types
npm run check
npm run build
python3 .labex/verify.py build
Wrangler leitet Cloudflare.Env aus den tatsächlichen Bindings ab. Dadurch kann sich keine manuell geschriebene Umgebungsschnittstelle von wrangler.jsonc entfernen.
Workers AI ist ein Remote-Binding. Die lokale Laufzeit benötigt daher den bereits von Wrangler gespeicherten OAuth-Zugriff. Übergeben Sie ihn nur an den untergeordneten Prozess und löschen Sie anschließend sofort die Kopie in der Shell:
DEV_PROXY_TOKEN="$(npx wrangler auth token --json | node -e 'let data="";process.stdin.on("data",chunk=>data+=chunk).on("end",()=>process.stdout.write(JSON.parse(data).token))')"
CLOUDFLARE_API_TOKEN="$DEV_PROXY_TOKEN" CI=true npm run dev > .labex/dev.log 2>&1 < /dev/null &
echo $! > .labex/dev.pid
unset DEV_PROXY_TOKEN
for attempt in $(seq 1 40); do
curl --silent --fail http://127.0.0.1:5173/ > /dev/null && break
sleep 1
done
tail -n 12 .labex/dev.log
python3 .labex/verify.py local
Geben Sie den temporären OAuth-Wert nicht aus und speichern Sie ihn nicht in .dev.vars. Die unabhängige Prüfung verwendet einen zufälligen benannten Agent, die schreibgeschützte Methode inspectCase() und einen nur für Tests bestimmten HMAC-Nachweis, der aus dem lokalen Signiergeheimnis abgeleitet wird. Dieser Nachweis ermöglicht es dem Verifizierer, denselben privaten Effekt zu testen, den das genehmigte Modell-Tool verwendet, ohne ein Callable zu veröffentlichen, das die Genehmigung umgehen kann. So wird die serverseitige Sicherheit des Effekts unabhängig vom Modellverhalten nachgewiesen:
- Die anfängliche Priorität lautet
medium, die Revision ist0. - Eine Leseabfrage über eine andere Warteschlange schlägt fehl.
- Ein Aufruf des genehmigten Effekts setzt die Priorität auf
highund die Revision auf1. - Derselbe Operationsschlüssel gibt
duplicate: truezurück, während die Revision1bleibt. - Ein anderer benannter Agent behält seinen isolierten Datensatz mit Revision
0.
Dieser deterministische Test beantwortet die Frage, ob wiederholte Zustellungen sicher sind. Der Browser-Ablauf weist separat nach, dass die unterstützte SDK-Sperre den Effekt vor der Genehmigung und bei einer Ablehnung verhindert.
Ablehnung, Genehmigung und Wiederholung bereitstellen und testen
In diesem Schritt stellen Sie die Anwendung bereit und beobachten anschließend, wie dieselbe vorgeschlagene Änderung ausstehend bleibt, abgelehnt, genau einmal genehmigt und bei einer Wiederholung sicher behandelt wird.
Stellen Sie das Produktions-Bundle bereit und laden Sie den erzeugten Signierschlüssel als Secret hoch:
npm run deploy
npx wrangler secret bulk .dev.vars
Der Secret-Befehl überträgt den Wert, ohne ihn in der Konfiguration oder im Bundle abzulegen. Geben Sie .dev.vars nicht aus.
Speichern Sie den bei der Bereitstellung ausgegebenen genauen Origin und erstellen Sie ein zehn Minuten gültiges Token für planning:
WORKER_URL="https://paste-the-workers-dev-origin-printed-by-deploy"
TOKEN="$(node scripts/create-session-token.mjs planning)"
printf '%s/?session=planning&token=%s\n' "${WORKER_URL%/}" "$TOKEN"
Öffnen Sie die vollständige URL im LabEx-Browser. Wählen Sie zuerst Check current case: Das schreibgeschützte Ergebnis meldet die Priorität medium und die Revision 0. Senden Sie anschließend die vorbereitete Änderungsanfrage. Dieser zweite, einzelne Durchlauf hält bei Approval required an. Vor einer Entscheidung wurde die serverseitige Funktion execute noch nicht ausgeführt:

Wählen Sie Deny. Das Tool wird abgelehnt, und der Agent darf nicht behaupten, dass die Aktualisierung stattgefunden hat. Wählen Sie erneut Check current case: Die neue Leseabfrage meldet weiterhin die Priorität medium und die Revision 0. Damit ist bewiesen, dass die abgelehnte Ausführung keine Änderung vorgenommen hat. Senden Sie die vorbereitete Änderungsanfrage erneut, um eine neue Genehmigungskarte zu erzeugen:

Wählen Sie bei dieser zweiten Anfrage Approve. Der unterstützte Client sendet die Genehmigungs-ID an den Agent zurück. Der Server führt die Änderung einmal aus, und das Ergebnis meldet die Priorität high, die Revision 1 sowie duplicate: false. Wählen Sie Check current case, um dieselbe Revision unabhängig zu beobachten:

Senden Sie dieselbe vorbereitete Änderungsanfrage ein drittes Mal und genehmigen Sie sie. Das dauerhafte Protokoll erkennt raise-synthetic-priority; das Ergebnis meldet duplicate: true. Wählen Sie erneut Check current case. Der Datensatz bleibt bei Revision 1:

Die genauen Sätze des Assistenten werden vom Modell erzeugt und können abweichen. Aussagekräftig sind der Zustand der Genehmigungskarte, die Felder des Tool-Ergebnisses und die Revision des Datensatzes. Warteschlange, Ticket und Datensatz sind synthetische Beispiele.
Führen Sie eine neue, unabhängig benannte Cloud-Prüfung aus. Sie verbraucht keinen weiteren Modellaufruf:
python3 .labex/verify.py deployed
Die Prüfung kontrolliert die exakt bereitgestellten Bindings und den Namespace. Anschließend wiederholt sie die Ablehnung eines Zugriffs außerhalb des Gültigkeitsbereichs, eine erfolgreiche Änderung, die sichere Wiederholung mit identischem Schlüssel und die Isolation benannter Agents anhand eines unabhängig benannten Remote-Agents. Sie testet den idempotenten Effekt, ohne einen weiteren Modellaufruf zu verbrauchen; der Browser-Ablauf weist die SDK-Genehmigungssperre nach.
Genehmigungsressourcen prüfen und entfernen
In diesem Schritt ordnen Sie das Laufzeitverhalten den Ressourcenansichten von Cloudflare zu und löschen anschließend nur die Ressourcen dieses Labs.
Öffnen Sie im Cloudflare-Dashboard Workers & Pages, wählen Sie Ihren exakten Worker labex-c11-s06-... aus und prüfen Sie Bindings. Sie sollten das Workers-AI-Binding AI und das Durable-Object-Binding ApprovalAgent sehen. Öffnen Sie anschließend Settings > Variables and Secrets, um zu bestätigen, dass SESSION_SIGNING_KEY als verschlüsseltes Secret und nicht als Klartext gespeichert ist:

Öffnen Sie Durable Objects und wählen Sie den SQL-basierten Namespace aus, der diesem Worker gehört. planning und die Namen des Verifizierers sind separate Objektinstanzen innerhalb dieses einen Klassen-Namespaces:

Öffnen Sie die Worker-Logs oder die Observability-Ansicht und suchen Sie nach approval_change_applied. Eine akzeptierte logische Operation erzeugt genau einen strukturierten Eintrag. Er enthält die Agent-Instanz, den stabilen Operationsschlüssel und die Revision, aber weder die Zusammenfassung des synthetischen Falls noch den Chattext:

Erstellen Sie nach der Prüfung eine ausdrückliche Migration zum Löschen der Klasse und entfernen Sie den exakten Worker:
python3 - <<'PY'
import json
from pathlib import Path
path = Path('wrangler.jsonc')
data = json.loads(path.read_text())
data.pop('durable_objects', None)
data['migrations'].append({'tag': 'v2', 'deleted_classes': ['ApprovalAgent']})
Path('wrangler.cleanup.jsonc').write_text(json.dumps(data, indent=2) + '\n')
PY
npx wrangler deploy --config wrangler.cleanup.jsonc
npx wrangler delete --config wrangler.cleanup.jsonc --force
Bestätigen Sie, dass der nicht dauerhafte Worker nicht mehr vorhanden ist:

Bestätigen Sie anschließend, dass sein ApprovalAgent-Namespace nicht mehr vorhanden ist:

Weisen Sie beide Abwesenheiten nach, solange diese VM noch autorisiert ist:
python3 .labex/verify.py deleted
Wenn Sie nur den Worker löschen, bleibt der Lebenszyklus der zustandsbehafteten Klasse unklar. Die Migration v2 entfernt den Namespace, den synthetischen Datensatz und das Idempotenzprotokoll dieses Labs ausdrücklich, bevor die Löschung des Workers geprüft wird.
Autorisierung dieser VM widerrufen
In diesem Schritt widerrufen Sie die temporäre VM-Autorisierung, nachdem die Bereinigung der Cloud-Ressourcen nachgewiesen wurde.
npx wrangler logout
npx wrangler whoami --json || true
Das strukturierte Ergebnis sollte "loggedIn": false melden. Alternativ kann Wrangler ein Ergebnis mit einem von null verschiedenen Status für eine nicht authentifizierte Sitzung zurückgeben. Die Abmeldung erfolgt absichtlich zuletzt: Der Löschverifizierer benötigt gültigen Lesezugriff, die verworfene VM jedoch nicht mehr.
Zusammenfassung
Sie haben eine unterstützte menschliche Entscheidung vor eine Datensatzänderung in einem Cloudflare-AIChatAgent gestellt. Sie haben:
- den Lesevorgang automatisch ausgeführt und den Schreibvorgang mit
needsApprovalmarkiert; - Teile mit dem Zustand
approval-requestedangezeigt und ausdrückliche Genehmigungs- oder Ablehnungsantworten übermittelt; - nachgewiesen, dass ausstehende und abgelehnte Operationen den synthetischen Datensatz unverändert lassen;
- einen dauerhaften Idempotenzschlüssel gespeichert, sodass eine genehmigte Wiederholung Revision
1zurückgibt, statt erneut zu schreiben; - die Autorisierung getrennt gehalten, indem der benannte Agent den Gültigkeitsbereich serverseitig erzwingt;
- ein datenschutzbeschränktes Genehmigungsereignis beobachtet; und
- den exakten SQLite-Klassen-Namespace und den Worker gelöscht, bevor Sie sich abgemeldet haben.
Die menschliche Genehmigung ist nun ausdrücklich und auditierbar, während das Idempotenzprotokoll den Effekt vor wiederholten Zustellungen schützt. Im nächsten Lab ändert sich die Richtung: Sie veröffentlichen eine synthetische schreibgeschützte Funktion über MCP. Dabei werden Erkennung und Transport zu den neuen Konzepten.



