Update modifications
This commit is contained in:
parent
d0853db245
commit
a021bd4fbc
13 changed files with 103 additions and 10 deletions
|
|
@ -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 <nom>` · `/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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)"],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -46,8 +46,8 @@ export async function initializeAgent(ctx: InitContext): Promise<ModeInitResult>
|
|||
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<ModeInitResult>
|
|||
commands: [],
|
||||
skills: toAgentSkills(skills),
|
||||
permissions: createPermissionPolicy(settings.rules),
|
||||
permissionSources: settings.files.filter((f) => f.allow.length + f.ask.length + f.deny.length > 0),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>)[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])];
|
||||
|
|
|
|||
|
|
@ -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 <nom> /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<void> {
|
|||
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 <nouveau nom>)`);
|
||||
|
|
|
|||
|
|
@ -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<string, string>;
|
||||
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: [],
|
||||
|
|
|
|||
|
|
@ -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<string, string>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <Text dimColor>Aucune règle de permission définie : chaque utilisation d'un outil demande une confirmation.</Text>;
|
||||
}
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{files.map((file, i) => (
|
||||
<Box key={file.path} flexDirection="column" marginTop={i === 0 ? 0 : 1}>
|
||||
<Text bold>{`${file.path} :`}</Text>
|
||||
{PERMISSION_KINDS.filter((k) => file[k.key].length > 0).map((k) => (
|
||||
<Box key={k.key} flexDirection="column">
|
||||
<Badge color={k.color}>{k.label}</Badge>
|
||||
{file[k.key].map((rule) => (
|
||||
<Text key={rule}>{rule}</Text>
|
||||
))}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function LogLine({ item }: { item: LogItem }) {
|
||||
switch (item.kind) {
|
||||
case "intro":
|
||||
|
|
@ -78,6 +109,8 @@ function LogLine({ item }: { item: LogItem }) {
|
|||
);
|
||||
case "assistant":
|
||||
return <Text>{item.text}</Text>;
|
||||
case "permissions":
|
||||
return <PermissionsView files={item.permissions ?? []} />;
|
||||
case "shell":
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<T = unknown> {
|
||||
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue