feat: add orchestrator policy inheritance

This commit is contained in:
artale 2026-06-15 17:15:18 +02:00
parent 92ebedc703
commit fa5006ed98
3 changed files with 56 additions and 0 deletions

View File

@ -37,3 +37,6 @@ export type { SubAgentSpec, MetaAgentConfig, MetaAgentResult } from "./meta-agen
export { AgentTeams } from "./agent-teams.js";
export type { TeamRole, TeamAgent, TeamConfig, TeamResult } from "./agent-teams.js";
export { isActionDenied, mergeToolPolicies } from "./orchestrator-policy.js";
export type { ToolPolicy, EffectiveToolPolicy } from "./orchestrator-policy.js";

View File

@ -0,0 +1,20 @@
import { describe, expect, it } from "vitest";
import { isActionDenied, mergeToolPolicies } from "./orchestrator-policy.js";
describe("orchestrator tool policy", () => {
it("cascades parent denies over child allows", () => {
const effective = mergeToolPolicies(
{ deny: ["git push --force"] },
{ allow: ["shell", "git push --force"] },
);
expect(effective.allow).toContain("git push --force");
expect(effective.deny).toContain("git push --force");
expect(isActionDenied("git push origin main --force", effective)).toBe(true);
expect(isActionDenied("git push origin main --force-with-lease", effective)).toBe(true);
});
it("does not block normal push when only force push is denied", () => {
expect(isActionDenied("git push origin main", { deny: ["git push --force"] })).toBe(false);
});
});

View File

@ -0,0 +1,33 @@
export interface ToolPolicy {
allow?: string[];
deny?: string[];
}
export interface EffectiveToolPolicy {
allow: string[];
deny: string[];
}
const uniq = (items: string[] = []): string[] => [...new Set(items.map((s) => s.trim()).filter(Boolean))];
export function mergeToolPolicies(parent?: ToolPolicy, child?: ToolPolicy): EffectiveToolPolicy {
return {
allow: uniq([...(parent?.allow ?? []), ...(child?.allow ?? [])]),
// ponytail: deny wins; add richer policy language when real agent configs need it.
deny: uniq([...(parent?.deny ?? []), ...(child?.deny ?? [])]),
};
}
export function isActionDenied(action: string, policy?: ToolPolicy): boolean {
const normalized = action.toLowerCase().replace(/\s+/g, " ").trim();
return mergeToolPolicies(policy).deny.some((rule) => matchesRule(normalized, rule));
}
function matchesRule(action: string, rule: string): boolean {
const normalizedRule = rule.toLowerCase().replace(/\s+/g, " ").trim();
if (!normalizedRule) return false;
if (normalizedRule === "git push --force") {
return /\bgit\s+push\b/.test(action) && /(?:\s|^)(?:--force(?:\s|=|$)|-f(?:\s|$)|--force-with-lease(?:\s|=|$))/.test(action);
}
return action.includes(normalizedRule);
}