feat(permissions): voie projection CLI (LP0→LP3) + checkpoint Codex/input
Jalon vert regroupant deux chantiers entrelacés dans le working tree, indissociables au niveau fichier mais tous deux verts (cargo test --workspace + tests frontend permissions au vert). Permissions — voie « projection CLI » (advisory), complète : - LP0 domaine pur : modèle PermissionSet/EffectivePermissions, resolve deny-wins + postures Allow<Ask<Deny (crates/domain/src/permission.rs). - LP1 store : FsPermissionStore (.ideai/permissions.json). - LP2 use cases : Get/Update project, Update agent override, Resolve. - LP3 projecteurs Claude/Codex (settings.local.json / config.toml), câblage launch-path + PermissionProjectorRegistry, nettoyage des fichiers Replace orphelins au swap de profil (LP3-4), composition root + commandes Tauri, UI PermissionsPanel (projet + override agent). - ports.rs : PermissionStore + FileSystem::remove_file (cleanup au swap). Reste ouvert (hors scope, marqué dans le code) : LP4 enforcement OS airtight (Landlock fichiers) + résumé de permissions injecté. Inclut aussi le chantier Codex/input/sessions structurées en cours (McpConfigStrategy, StructuredAdapter, gestion d'input) partageant les mêmes fichiers (lifecycle.rs, commands.rs, dto.rs, state.rs). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
236
frontend/src/features/permissions/usePermissions.ts
Normal file
236
frontend/src/features/permissions/usePermissions.ts
Normal file
@ -0,0 +1,236 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { useGateways } from "@/app/di";
|
||||
import type {
|
||||
Agent,
|
||||
Capability,
|
||||
PermissionEffect,
|
||||
PermissionPosture,
|
||||
PermissionRule,
|
||||
PermissionSet,
|
||||
ProjectPermissions,
|
||||
} from "@/domain";
|
||||
|
||||
export type CapabilityChoice = "none" | PermissionEffect;
|
||||
|
||||
export interface PolicyDraft {
|
||||
fallback: PermissionPosture;
|
||||
read: CapabilityChoice;
|
||||
write: CapabilityChoice;
|
||||
delete: CapabilityChoice;
|
||||
executeBash: CapabilityChoice;
|
||||
}
|
||||
|
||||
export interface AgentPermissionRow {
|
||||
agent: Agent;
|
||||
override: PermissionSet | null;
|
||||
}
|
||||
|
||||
export interface PermissionsViewModel {
|
||||
agents: Agent[];
|
||||
rows: AgentPermissionRow[];
|
||||
document: ProjectPermissions | null;
|
||||
projectDraft: PolicyDraft;
|
||||
busy: boolean;
|
||||
error: string | null;
|
||||
refresh: () => Promise<void>;
|
||||
saveProjectDefaults: (draft: PolicyDraft) => Promise<void>;
|
||||
clearProjectDefaults: () => Promise<void>;
|
||||
saveAgentOverride: (agentId: string, draft: PolicyDraft) => Promise<void>;
|
||||
clearAgentOverride: (agentId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const DEFAULT_DRAFT: PolicyDraft = {
|
||||
fallback: "ask",
|
||||
read: "none",
|
||||
write: "none",
|
||||
delete: "none",
|
||||
executeBash: "none",
|
||||
};
|
||||
|
||||
const CAPABILITIES: Capability[] = ["read", "write", "delete", "executeBash"];
|
||||
|
||||
function describe(e: unknown): string {
|
||||
if (e && typeof e === "object" && "message" in e) {
|
||||
return String((e as { message: unknown }).message);
|
||||
}
|
||||
return String(e);
|
||||
}
|
||||
|
||||
export function draftFromSet(set?: PermissionSet | null): PolicyDraft {
|
||||
if (!set) return { ...DEFAULT_DRAFT };
|
||||
const draft: PolicyDraft = {
|
||||
...DEFAULT_DRAFT,
|
||||
fallback: set.fallback,
|
||||
};
|
||||
for (const capability of CAPABILITIES) {
|
||||
draft[capability] = ruleChoice(set.rules, capability);
|
||||
}
|
||||
return draft;
|
||||
}
|
||||
|
||||
function ruleChoice(
|
||||
rules: PermissionRule[],
|
||||
capability: Capability,
|
||||
): CapabilityChoice {
|
||||
const matching = rules.filter((rule) => rule.capability === capability);
|
||||
if (matching.some((rule) => rule.effect === "deny")) return "deny";
|
||||
if (matching.some((rule) => rule.effect === "allow")) return "allow";
|
||||
return "none";
|
||||
}
|
||||
|
||||
export function setFromDraft(draft: PolicyDraft): PermissionSet {
|
||||
const rules: PermissionRule[] = [];
|
||||
for (const capability of CAPABILITIES) {
|
||||
const effect = draft[capability];
|
||||
if (effect === "none") continue;
|
||||
rules.push(ruleFromChoice(capability, effect));
|
||||
}
|
||||
return { fallback: draft.fallback, rules };
|
||||
}
|
||||
|
||||
function ruleFromChoice(
|
||||
capability: Capability,
|
||||
effect: PermissionEffect,
|
||||
): PermissionRule {
|
||||
if (capability === "executeBash") {
|
||||
return { capability, effect, commands: [] };
|
||||
}
|
||||
return { capability, effect, paths: ["**"] };
|
||||
}
|
||||
|
||||
export function isCustomPolicy(set?: PermissionSet | null): boolean {
|
||||
if (!set) return false;
|
||||
return set.rules.some((rule) => {
|
||||
if (rule.capability === "executeBash") {
|
||||
return (rule.commands ?? []).length > 0;
|
||||
}
|
||||
const paths = rule.paths ?? [];
|
||||
return paths.length !== 1 || paths[0] !== "**";
|
||||
});
|
||||
}
|
||||
|
||||
export function usePermissions(projectId: string): PermissionsViewModel {
|
||||
const { agent, permission } = useGateways();
|
||||
const [agents, setAgents] = useState<Agent[]>([]);
|
||||
const [document, setDocument] = useState<ProjectPermissions | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
const [agentList, permissionDoc] = await Promise.all([
|
||||
agent.listAgents(projectId),
|
||||
permission.getProjectPermissions(projectId),
|
||||
]);
|
||||
setAgents(agentList);
|
||||
setDocument(permissionDoc);
|
||||
} catch (e) {
|
||||
setError(describe(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}, [agent, permission, projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
}, [refresh]);
|
||||
|
||||
const rows = useMemo<AgentPermissionRow[]>(() => {
|
||||
const overrides = new Map(
|
||||
(document?.agents ?? []).map((entry) => [entry.agentId, entry.permissions]),
|
||||
);
|
||||
return agents.map((candidate) => ({
|
||||
agent: candidate,
|
||||
override: overrides.get(candidate.id) ?? null,
|
||||
}));
|
||||
}, [agents, document]);
|
||||
|
||||
const projectDraft = useMemo(
|
||||
() => draftFromSet(document?.projectDefaults ?? null),
|
||||
[document],
|
||||
);
|
||||
|
||||
const saveProjectDefaults = useCallback(
|
||||
async (draft: PolicyDraft) => {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
setDocument(
|
||||
await permission.updateProjectPermissions(projectId, setFromDraft(draft)),
|
||||
);
|
||||
} catch (e) {
|
||||
setError(describe(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
},
|
||||
[permission, projectId],
|
||||
);
|
||||
|
||||
const clearProjectDefaults = useCallback(async () => {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
setDocument(await permission.updateProjectPermissions(projectId, null));
|
||||
} catch (e) {
|
||||
setError(describe(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}, [permission, projectId]);
|
||||
|
||||
const saveAgentOverride = useCallback(
|
||||
async (agentId: string, draft: PolicyDraft) => {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
setDocument(
|
||||
await permission.updateAgentPermissions(
|
||||
projectId,
|
||||
agentId,
|
||||
setFromDraft(draft),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
setError(describe(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
},
|
||||
[permission, projectId],
|
||||
);
|
||||
|
||||
const clearAgentOverride = useCallback(
|
||||
async (agentId: string) => {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
setDocument(
|
||||
await permission.updateAgentPermissions(projectId, agentId, null),
|
||||
);
|
||||
} catch (e) {
|
||||
setError(describe(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
},
|
||||
[permission, projectId],
|
||||
);
|
||||
|
||||
return {
|
||||
agents,
|
||||
rows,
|
||||
document,
|
||||
projectDraft,
|
||||
busy,
|
||||
error,
|
||||
refresh,
|
||||
saveProjectDefaults,
|
||||
clearProjectDefaults,
|
||||
saveAgentOverride,
|
||||
clearAgentOverride,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user