Construire

Autorisations

Liez chaque accord à une action, une ressource et une durée précises — et laissez votre agent refuser tout le reste.

Un widget d'autorisation enregistre que l'utilisateur a dit oui. La politique d'autorisation va plus loin : elle rattache ce oui à une opération précise et ne laisse votre code l'exécuter que tant que l'accord est valable.

Utilisez-la dès que votre agent peut toucher à quelque chose d'important — fichiers, commandes, comptes, argent.

Principe

  • Chaque demande reçoit un identifiant de widget unique et décrit une action, une ressource et des paramètres exacts — par exemple read_file sur /home/moi/rapport.txt en utf8.
  • L'accord est lié à l'utilisateur, à la conversation et à une session nommée qui expire. Les réponses doivent arriver par le canal chiffré du SDK.
  • Votre code exécute l'opération via runAuthorized (run_authorized). La politique refuse tout ce qui ne correspond pas exactement à l'accord.
PortéeSignification
onceUne seule tentative. Consommée avant le lancement de votre fonction — même si elle échoue.
sessionTentatives répétées de la même opération, ressource et paramètres, jusqu'à l'expiration, la révocation ou la fin de session.

Les accords vivent en mémoire. Un redémarrage exige un nouvel accord — c'est voulu.

Exemple : lire un fichier, avec accord

import { readFile, realpath } from "node:fs/promises";
import { PermissionPolicy } from "@pacerelle/sdk/permissions";

const session = {
  id: crypto.randomUUID(),
  label: "Lecture des rapports locaux",
  expiresAt: new Date(Date.now() + 30 * 60_000), // 30 minutes
};
const permissions = new PermissionPolicy({ session });
const pending = new Map(); // identifiant du widget → opération demandée

client.onMessage(async (message, agent) => {
  const send = (text) =>
    agent.sendMessage({
      conversationId: message.conversationId,
      to: message.from,
      toDeviceId: message.fromDeviceId,
      text,
    });
  // Aligne la carte affichée dans l'application sur ce que la politique a réellement fait.
  const report = (ref) => send(JSON.stringify(permissions.createWidgetUpdate(ref)));

  // 1. Est-ce une réponse à l'une de nos demandes ?
  const decision = permissions.receiveResponse(message);
  if (decision.status === "denied" || decision.status === "revoked") {
    await report(decision.requestId);
    return;
  }
  if (decision.status === "granted") {
    const task = pending.get(decision.grant.requestId);
    if (!task) return; // ne jamais déduire l'opération de la réponse elle-même
    await report(decision.grant.requestId);
    try {
      const text = await permissions.runAuthorized(
        decision.grant.id,
        { ...task, userId: message.from, conversationId: message.conversationId, sessionId: session.id },
        (allowed) => readFile(allowed.resource, "utf8"),
      );
      await send(`C'est fait — le rapport compte ${text.length} caractères.`);
    } finally {
      await report(decision.grant.requestId);
    }
    return;
  }
  if (message.widgetResponse) return; // réponse sans rapport, rejouée ou invalide

  // 2. Une demande qui nécessite un accord.
  if (message.text !== "Lis le rapport") return;
  const task = {
    action: "read_file",
    resource: await realpath("./rapport.txt"), // résoudre avant de demander
    parameters: { encoding: "utf8" },
  };
  const widget = permissions.createRequest({
    ...task,
    conversationId: message.conversationId,
    userId: message.from,
    title: "Lire ce rapport ?",
    body: "L'agent lira uniquement ce fichier, en texte.",
    expiresAt: new Date(Date.now() + 5 * 60_000),
    scopes: ["once", "session"],
  });
  pending.set(widget.id, task);
  await send(JSON.stringify(widget));
});

Envoyez le widget renvoyé par createRequest comme texte d'un message, comme ci-dessus. L'assistant plus simple sendPermissionWidget n'ajoute pas les détails d'autorisation de la politique.

Révoquer et terminer une session

  • L'utilisateur peut révoquer un accord depuis l'application après l'avoir donné. receiveResponse vérifie l'auteur et la conversation, puis bloque toute utilisation future. Une opération déjà lancée ne peut pas être interrompue.
  • Dans le code, revoke(grantId) retire un accord, endSession() (end_session()) les retire tous et listGrants() (list_grants()) liste ceux qui sont actifs.

Règles d'or

  • Résolvez avant de demander. Transformez alias et chemins relatifs en ressource réelle, et incluez dans parameters chaque argument qui change l'effet.
  • Ne tirez jamais l'opération de la réponse. Retrouvez-la dans votre propre table pending.
  • Gardez des sessions courtes. Des minutes, pas des jours. Il n'existe volontairement pas de portée « pour toujours ».
  • Rendez compte honnêtement. Un accord ne prouve pas que l'opération a réussi — dites à l'utilisateur ce qui s'est réellement passé.