From a021bd4fbc0d1a88fb1b87ba27e899be97fdbc3c Mon Sep 17 00:00:00 2001 From: Arnaud Date: Thu, 15 Oct 2026 23:49:58 +0200 Subject: [PATCH] Update modifications --- README.md | 4 ++++ packages/agent/src/commands.ts | 2 +- packages/agent/src/init.test.ts | 8 ++++++++ packages/agent/src/init.ts | 5 +++-- packages/agent/src/settings.ts | 8 ++++---- packages/cli/src/index.ts | 6 +++++- packages/core/src/agent.ts | 4 ++++ packages/core/src/types.ts | 10 ++++++++++ packages/ui/src/components.tsx | 33 +++++++++++++++++++++++++++++++++ packages/ui/src/index.ts | 2 +- packages/ui/src/ink-ui.tsx | 5 +++++ packages/ui/src/store.test.ts | 9 +++++++++ packages/ui/src/store.ts | 17 ++++++++++++++++- 13 files changed, 103 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 509b0a7..90182d8 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ Il n'y a plus de notion de mode ni d'extensions : un seul agent, MAIA. | Saisie | Effet | | --- | --- | | `/help` | aide (commandes, skills) | +| `/permissions` | affiche les règles de permissions chargées, par fichier : badges `ALLOW`, `ASK`, `DENY` suivis des règles | | `/model` | recharge la liste des modèles LiteLLM et change de modèle (conversation conservée) | | `/clear` | supprime le contexte de la session et recharge `MAIA.md` / `CLAUDE.md` et les skills, comme au démarrage (la session garde son nom) | | `/rename ` · `/load [nom]` · `/new` | sessions (voir plus bas) | @@ -246,6 +247,8 @@ Au démarrage, MAIA indique (`INFO`) les fichiers chargés et le nombre de règl Attention : `./.maia/settings.json` d'un dépôt tiers peut contenir des règles `allow` larges ; relisez-le avant de lancer MAIA dans un projet que vous ne connaissez pas. +La commande `/permissions` affiche les règles actives, fichier par fichier, avec un badge `ALLOW` (vert), `ASK` (jaune) et `DENY` (rouge) suivi des règles du type, une par ligne ; seuls les types non vides sont montrés. Après avoir modifié un `settings.json`, `/clear` recharge la configuration. + Exemple pour éviter les confirmations sur les outils en lecture seule : voir `settings.example.json`. ## Outils TodoWrite, WebFetch et CurrentModel @@ -290,3 +293,4 @@ Journal des évolutions, dans l'ordre : 17. **Outil `CurrentModel`** : indique au modèle quel LLM est utilisé dans la session. 18. **`/clear`** : supprime le contexte et recharge `MAIA.md` / `CLAUDE.md` (et les skills) comme au démarrage ; `/new` fait de même. 19. **Permissions granulaires** : règles `allow` / `ask` / `deny` par outil (`Outil` ou `Outil(motif)`), évaluées dans l'ordre deny > ask > allow, `ask` par défaut ; définies dans `~/.maia/settings.json` et `./.maia/settings.json` (pas le dossier parent). +20. **`/permissions`** : affiche les permissions chargées, par fichier et par type (badges ALLOW / ASK / DENY) ; les fichiers sont listés l'un sous l'autre au démarrage. diff --git a/packages/agent/src/commands.ts b/packages/agent/src/commands.ts index 1499a46..be69fb9 100644 --- a/packages/agent/src/commands.ts +++ b/packages/agent/src/commands.ts @@ -4,7 +4,7 @@ import type { SlashCommand } from "@maia/core"; import { parseFrontmatter } from "./frontmatter"; /** Noms réservés aux commandes internes de MAIA (gérées par la CLI). */ -export const RESERVED_COMMANDS = new Set(["help", "clear", "model", "exit", "rename", "load", "new"]); +export const RESERVED_COMMANDS = new Set(["help", "clear", "model", "exit", "rename", "load", "new", "permissions"]); async function listMarkdown(dir: string, prefix = ""): Promise<{ name: string; file: string }[]> { let entries; diff --git a/packages/agent/src/init.test.ts b/packages/agent/src/init.test.ts index b04b72d..fd6b8ae 100644 --- a/packages/agent/src/init.test.ts +++ b/packages/agent/src/init.test.ts @@ -103,6 +103,14 @@ describe("permissions au démarrage", () => { expect(logs.join("\n")).toContain("Permissions chargées"); expect(logs.join("\n")).toContain("2 deny, 1 ask, 1 allow"); expect(logs.some((l) => l.startsWith("Permissions chargées :\n"))).toBe(true); // un fichier par ligne + expect(init.permissionSources).toEqual([ + { + path: join(project, ".maia", "settings.json"), + allow: ["Bash(allow stop *)"], + ask: ["Bash(allow ask *)"], + deny: ["Bash(allow deny *)", "Read(./.deny.md)"], + }, + ]); }); }); diff --git a/packages/agent/src/init.ts b/packages/agent/src/init.ts index 35af5e8..e797cb1 100644 --- a/packages/agent/src/init.ts +++ b/packages/agent/src/init.ts @@ -46,8 +46,8 @@ export async function initializeAgent(ctx: InitContext): Promise const { allow, ask, deny } = settings.rules; if (allow.length + ask.length + deny.length > 0) { const files = settings.files - .filter((f) => f.allow + f.ask + f.deny > 0) - .map((f) => `${f.path} (${f.deny} deny, ${f.ask} ask, ${f.allow} allow)`); + .filter((f) => f.allow.length + f.ask.length + f.deny.length > 0) + .map((f) => `${f.path} (${f.deny.length} deny, ${f.ask.length} ask, ${f.allow.length} allow)`); ctx.log(["Permissions chargées :", ...files].join("\n")); } else { ctx.log("Aucune règle de permission définie : chaque utilisation d'un outil demande une confirmation."); @@ -60,5 +60,6 @@ export async function initializeAgent(ctx: InitContext): Promise commands: [], skills: toAgentSkills(skills), permissions: createPermissionPolicy(settings.rules), + permissionSources: settings.files.filter((f) => f.allow.length + f.ask.length + f.deny.length > 0), }; } diff --git a/packages/agent/src/settings.ts b/packages/agent/src/settings.ts index ca4f42f..a97c881 100644 --- a/packages/agent/src/settings.ts +++ b/packages/agent/src/settings.ts @@ -10,7 +10,7 @@ export interface PermissionRules { export interface PermissionSettings { rules: PermissionRules; /** Fichiers settings.json réellement lus, avec le nombre de règles qu'ils apportent. */ - files: { path: string; allow: number; ask: number; deny: number }[]; + files: { path: string; allow: string[]; ask: string[]; deny: string[] }[]; } const KINDS = ["allow", "ask", "deny"] as const; @@ -58,7 +58,7 @@ export async function loadPermissionSettings( warnings.push(`${path} : "permissions" doit être un objet { "allow": [...], "ask": [...], "deny": [...] }`); continue; } - const counts = { allow: 0, ask: 0, deny: 0 }; + const own: { allow: string[]; ask: string[]; deny: string[] } = { allow: [], ask: [], deny: [] }; for (const kind of KINDS) { const list = (perms as Record)[kind]; if (list === undefined) continue; // les trois types sont facultatifs @@ -72,10 +72,10 @@ export async function loadPermissionSettings( continue; } settings.rules[kind].push(entry.trim()); - counts[kind]++; + own[kind].push(entry.trim()); } } - settings.files.push({ path, ...counts }); + settings.files.push({ path, ...own }); } for (const kind of KINDS) settings.rules[kind] = [...new Set(settings.rules[kind])]; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 9e12e3c..c019e58 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -65,7 +65,7 @@ Usage : maia [options] Sans cette option, la liste des modèles disponibles est proposée au démarrage. -h, --help Affiche cette aide -Commandes en session : /help /model /clear /exit +Commandes en session : /help /model /permissions /clear /exit Sessions : /rename /load [nom] /new (sauvegarde automatique à la sortie, dans ./.maia/.sessions) Skills de l'agent : §nom_du_skill [arguments] (§ seul : liste des skills) Commande shell : !commande ou !'commande avec paramètres' (exécutée directement ; résultat ajouté au contexte) @@ -273,6 +273,10 @@ async function run(ui: InkUI, flags: { model?: string }): Promise { ui.info([HELP, ...custom, ...skills].join("\n")); continue; } + if (name === "permissions") { + ui.showPermissions(session.permissionSources); + continue; + } if (name === "rename") { if (!sessions) ui.error("Sessions indisponibles (dossier ./.maia non accessible)."); else if (!args.trim()) ui.info(`Session courante : ${sessions.name} (usage : /rename )`); diff --git a/packages/core/src/agent.ts b/packages/core/src/agent.ts index 5ae5f66..7a448d2 100644 --- a/packages/core/src/agent.ts +++ b/packages/core/src/agent.ts @@ -6,6 +6,7 @@ import type { Message, ModeInitResult, PermissionPolicy, + PermissionSource, SlashCommand, Tool, } from "./types"; @@ -17,6 +18,8 @@ export interface AgentSession { commands: SlashCommand[]; skills: AgentSkill[]; permissions?: PermissionPolicy; + /** Fichiers dont viennent les règles de permissions (affichage avec /permissions). */ + permissionSources: PermissionSource[]; env?: Record; messages: Message[]; /** Contexte ajouté par l'utilisateur hors conversation (ex. résultat d'une commande "!"), joint à sa prochaine demande. */ @@ -38,6 +41,7 @@ export function createSession(opts: { commands: opts.init.commands ?? [], skills: opts.init.skills ?? [], permissions: opts.init.permissions, + permissionSources: opts.init.permissionSources ?? [], env: opts.init.env, messages: [], pendingContext: [], diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 6b955f9..483291c 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -122,6 +122,14 @@ export interface InitContext { warn(message: string): void; } +/** Règles de permissions apportées par un fichier (pour l'affichage avec /permissions). */ +export interface PermissionSource { + path: string; + allow: string[]; + ask: string[]; + deny: string[]; +} + /** Résultat de l'initialisation de l'agent : ce dont l'agent a besoin pour démarrer. */ export interface ModeInitResult { systemPrompt: string; @@ -132,6 +140,8 @@ export interface ModeInitResult { skills?: AgentSkill[]; /** Règles allow/ask/deny de l'agent (remplacent la permission par défaut des outils quand elles s'appliquent). */ permissions?: PermissionPolicy; + /** Fichiers dont viennent les règles de `permissions`, pour l'affichage. */ + permissionSources?: PermissionSource[]; /** Variables d'environnement transmises aux outils (ex. Bash). */ env?: Record; } diff --git a/packages/ui/src/components.tsx b/packages/ui/src/components.tsx index cb58702..7294ac0 100644 --- a/packages/ui/src/components.tsx +++ b/packages/ui/src/components.tsx @@ -9,6 +9,7 @@ import type { ConfirmPromptState, InputPromptState, LogItem, + PermissionFile, PromptState, SelectPromptState, UIStore, @@ -16,6 +17,36 @@ import type { // ───────────── Journal ───────────── +const PERMISSION_KINDS = [ + { key: "allow", label: "ALLOW", color: "green" }, + { key: "ask", label: "ASK", color: "yellow" }, + { key: "deny", label: "DENY", color: "red" }, +] as const; + +/** Règles par fichier : chemin, puis un badge ALLOW / ASK / DENY suivi des règles du type (une par ligne). */ +function PermissionsView({ files }: { files: PermissionFile[] }) { + if (files.length === 0) { + return Aucune règle de permission définie : chaque utilisation d'un outil demande une confirmation.; + } + return ( + + {files.map((file, i) => ( + + {`${file.path} :`} + {PERMISSION_KINDS.filter((k) => file[k.key].length > 0).map((k) => ( + + {k.label} + {file[k.key].map((rule) => ( + {rule} + ))} + + ))} + + ))} + + ); +} + function LogLine({ item }: { item: LogItem }) { switch (item.kind) { case "intro": @@ -78,6 +109,8 @@ function LogLine({ item }: { item: LogItem }) { ); case "assistant": return {item.text}; + case "permissions": + return ; case "shell": return ( diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index df522d4..ba8ec5a 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -1,2 +1,2 @@ export { InkUI } from "./ink-ui"; -export { UIStore, type Choice, type LogItem, type LogKind, type UIState } from "./store"; +export { UIStore, type Choice, type LogItem, type LogKind, type PermissionFile, type UIState } from "./store"; diff --git a/packages/ui/src/ink-ui.tsx b/packages/ui/src/ink-ui.tsx index 5fac3e2..046157f 100644 --- a/packages/ui/src/ink-ui.tsx +++ b/packages/ui/src/ink-ui.tsx @@ -86,6 +86,11 @@ export class InkUI implements AgentUI { this.start().log("user", text); } + /** Affiche les règles de permissions par fichier (allow / ask / deny). */ + showPermissions(files: PermissionFile[]): void { + this.start().showPermissions(files); + } + /** Commande "!" de l'utilisateur et son résultat. */ onShell(command: string, output: string, status: string | null): void { this.start().shell(command, output, status); diff --git a/packages/ui/src/store.test.ts b/packages/ui/src/store.test.ts index a21b103..ebc6f13 100644 --- a/packages/ui/src/store.test.ts +++ b/packages/ui/src/store.test.ts @@ -14,6 +14,15 @@ describe("UIStore", () => { expect(kinds(s)).toEqual(["assistant:Bonjour"]); }); + it("showPermissions : un élément avec les règles par fichier", () => { + const s = new UIStore(); + const files = [{ path: "/h/.maia/settings.json", allow: ["Read"], ask: [], deny: ["Bash(rm *)"] }]; + s.showPermissions(files); + const item = s.getSnapshot().items[0]!; + expect(item.kind).toBe("permissions"); + expect(item.permissions).toEqual(files); + }); + it("notifie les abonnés et fournit un instantané stable entre deux changements", () => { const s = new UIStore(); let calls = 0; diff --git a/packages/ui/src/store.ts b/packages/ui/src/store.ts index 365235f..a11ab86 100644 --- a/packages/ui/src/store.ts +++ b/packages/ui/src/store.ts @@ -13,7 +13,8 @@ export type LogKind = | "choice" | "user" | "assistant" - | "shell"; + | "shell" + | "permissions"; export interface LogItem { id: number; @@ -23,6 +24,15 @@ export interface LogItem { detail?: string; /** Commande shell en échec (affichée en rouge). */ failed?: boolean; + /** Règles de permissions par fichier (kind "permissions"). */ + permissions?: PermissionFile[]; +} + +export interface PermissionFile { + path: string; + allow: string[]; + ask: string[]; + deny: string[]; } export interface Choice { @@ -121,6 +131,11 @@ export class UIStore { this.commit({ items: this.withItem("shell", command, { detail, failed: status !== null }) }); } + /** Règles de permissions chargées, par fichier (commande /permissions). */ + showPermissions(files: PermissionFile[]): void { + this.commit({ items: this.withItem("permissions", "Permissions", { permissions: files }) }); + } + setBusy(label: string | null): void { this.commit({ busy: label }); }