Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions assistant/src/__tests__/checker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,15 @@ mock.module("../config/loader.js", () => ({
setNestedValue: () => {},
}));

// Mutable guardian persona path so tests can toggle whether
// getDefaultRuleTemplates emits the dynamic guardian-persona allow rules.
// Defaults to null so existing tests see no extra rules, matching the
// behaviour on a fresh install without a resolved guardian.
let mockGuardianPersonaPath: string | null = null;
mock.module("../prompts/persona-resolver.js", () => ({
resolveGuardianPersonaPath: () => mockGuardianPersonaPath,
}));

import {
check,
classifyRisk,
Expand Down Expand Up @@ -152,6 +161,8 @@ describe("Permission Checker", () => {
// Reset permissions mode to workspace (default) so existing tests are not affected
testConfig.permissions = { mode: "workspace" };
testConfig.skills = { load: { extraDirs: [] } };
// Reset guardian persona mock so each test opts in explicitly
mockGuardianPersonaPath = null;
loggerWarnCalls.length = 0;
try {
rmSync(join(checkerTestDir, "protected", "trust.json"));
Expand Down Expand Up @@ -1528,6 +1539,71 @@ describe("Permission Checker", () => {
// Low risk → auto-allowed even outside workspace
expect(result.decision).toBe("allow");
});

// ── guardian persona file (users/<slug>.md) ──────────────────
// The drop-user-md migration replaces the legacy workspace USER.md
// with a per-user persona file at `users/<guardian-slug>.md`. The
// dynamic guardian-persona default rules make first-run onboarding
// and day-to-day persona edits frictionless.

test("file_edit of guardian users/<slug>.md is auto-allowed", async () => {
const guardianPath = join(checkerTestDir, "users", "alice.md");
mockGuardianPersonaPath = guardianPath;
const result = await check("file_edit", { path: guardianPath }, "/tmp");
expect(result.decision).toBe("allow");
expect(result.matchedRule).toBeDefined();
expect(result.matchedRule!.id).toBe(
"default:allow-file_edit-guardian-persona",
);
});

test("file_read of guardian users/<slug>.md is auto-allowed", async () => {
const guardianPath = join(checkerTestDir, "users", "alice.md");
mockGuardianPersonaPath = guardianPath;
const result = await check("file_read", { path: guardianPath }, "/tmp");
expect(result.decision).toBe("allow");
expect(result.matchedRule).toBeDefined();
expect(result.matchedRule!.id).toBe(
"default:allow-file_read-guardian-persona",
);
});

test("file_write of guardian users/<slug>.md is auto-allowed", async () => {
const guardianPath = join(checkerTestDir, "users", "alice.md");
mockGuardianPersonaPath = guardianPath;
const result = await check("file_write", { path: guardianPath }, "/tmp");
expect(result.decision).toBe("allow");
expect(result.matchedRule).toBeDefined();
expect(result.matchedRule!.id).toBe(
"default:allow-file_write-guardian-persona",
);
});

test("getDefaultRuleTemplates emits guardian persona rules when guardian is resolved", () => {
const guardianPath = join(checkerTestDir, "users", "alice.md");
mockGuardianPersonaPath = guardianPath;
const templates = getDefaultRuleTemplates();
const guardianRules = templates.filter((t) =>
t.id.endsWith("-guardian-persona"),
);
// One rule each for file_read, file_write, file_edit.
expect(guardianRules).toHaveLength(3);
for (const rule of guardianRules) {
expect(rule.decision).toBe("allow");
expect(rule.priority).toBe(100);
expect(rule.scope).toBe("everywhere");
expect(rule.pattern).toBe(`${rule.tool}:${guardianPath}`);
}
});

test("getDefaultRuleTemplates emits no guardian persona rules when unresolved", () => {
mockGuardianPersonaPath = null;
const templates = getDefaultRuleTemplates();
const guardianRules = templates.filter((t) =>
t.id.endsWith("-guardian-persona"),
);
expect(guardianRules).toHaveLength(0);
});
});

// ── generateAllowlistOptions ───────────────────────────────────
Expand Down
30 changes: 30 additions & 0 deletions assistant/src/permissions/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { join } from "node:path";
import { getIsContainerized } from "../config/env-registry.js";
import { getConfig } from "../config/loader.js";
import { getBundledSkillsDir } from "../config/skills.js";
import { resolveGuardianPersonaPath } from "../prompts/persona-resolver.js";
import { getWorkspaceDir } from "../util/platform.js";

export interface DefaultRuleTemplate {
Expand Down Expand Up @@ -139,6 +140,34 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
})),
);

// Guardian persona file — the contact-store-resolved `users/<slug>.md`
// for the current guardian. Once the workspace has a guardian contact,
// their per-user persona file should be readable/editable without a
// prompt, the same way the legacy workspace USER.md is.
//
// This is resolved dynamically at template-build time (rather than
// hardcoded like WORKSPACE_PROMPT_FILES) because the slug depends on
// the installed guardian. The try/catch protects against early-boot
// paths where the DB may not yet be initialized — in that case the
// legacy workspace USER.md rules still cover onboarding.
let guardianPersonaRules: DefaultRuleTemplate[] = [];
try {
const guardianPath = resolveGuardianPersonaPath();
if (guardianPath) {
Comment thread
siddseethepalli marked this conversation as resolved.
const posixPath = guardianPath.replaceAll("\\", "/");
guardianPersonaRules = WORKSPACE_FILE_TOOLS.map((tool) => ({
id: `default:allow-${tool}-guardian-persona`,
tool,
pattern: `${tool}:${posixPath}`,
Comment thread
siddseethepalli marked this conversation as resolved.
scope: "everywhere",
decision: "allow" as const,
priority: 100,
}));
}
} catch {
// Guardian may not exist yet; the workspace prompt rules still cover USER.md during onboarding.
}

const bootstrapDeleteRule: DefaultRuleTemplate = {
id: "default:allow-bash-rm-bootstrap",
tool: "bash",
Expand Down Expand Up @@ -303,6 +332,7 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
...computerUseRules,
...managedSkillRules,
...workspacePromptRules,
...guardianPersonaRules,
bootstrapDeleteRule,
updatesDeleteRule,
...skillSourceMutationRules,
Expand Down
Loading