12 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 428 | pi-permission-system: permission-system using incorrect path for `projectAgentsDir` |
Fix project agents directory path resolution
Problem Statement
A user defined a project-scoped custom agent at <project>/.pi/agents/my-custom-agent.md with a permission: block in its YAML frontmatter. pi-subagents discovered and ran the agent correctly, but its permission frontmatter was never applied.
The config.resolved log shows why: pi-permission-system computed projectAgentsDir as <cwd>/.pi/agent/agents, then found nothing there and resolved no project-agent config.
The correct location — the one pi-subagents uses — is <cwd>/.pi/agents.
Goals
- Resolve the project agents directory at
<cwd>/.pi/agents, matching the Pi platform convention that pi-subagents already follows. - Extract a small named helper inside pi-permission-system that owns this convention, with a comment cross-referencing pi-subagents' sibling encoding.
- Add a regression test that exercises the real
agentDir-derived resolution end-to-end (a project agent'spermission:frontmatter is actually enforced). - Correct the project-agent override path documented in
docs/configuration.md, which currently repeats the same wrong path.
This change is breaking (fix!:).
On upgrade, project agents' permission: frontmatter — silently ignored today — starts being read and enforced without any user edit, which can make a session more restrictive than before.
Non-Goals
- No new shared package and no
pi-permission-system → pi-subagentscode dependency. The two packages are decoupled by design (event-bus only, ADR-0002); the path is a Pi platform convention that each client encodes independently, exactly as both already do for the global agents directory. - No change to pi-subagents — its
loadCustomAgents(config/custom-agents.ts) already resolves<cwd>/.pi/agentscorrectly. - No change to the global agents directory derivation (
<agentDir>/agents); it is already correct. - No change to the config schema, example config, or the
permission:frontmatter format.
Background
The resolution lives in src/permission-manager.ts.
derivePolicyLoaderOptions(agentDir, cwd) builds PolicyLoaderOptions for a FilePolicyLoader, and it is the sole place the project agents directory is derived from a cwd:
return {
globalConfigPath: getGlobalConfigPath(agentDir),
agentsDir: join(agentDir, "agents"),
projectGlobalConfigPath: cwd ? getProjectConfigPath(cwd) : undefined,
projectAgentsDir: cwd ? join(cwd, ".pi", "agent", "agents") : undefined, // ← bug
};
It is called from the PermissionManager constructor (when built with { agentDir }) and from configureForCwd(cwd).
The derived projectAgentsDir feeds FilePolicyLoader.loadProjectAgentConfig(agentName), which reads <projectAgentsDir>/<agentName>.md, extracts its frontmatter, and contributes the project-agent scope to the merged ruleset.
Pi-convention paths in this package already live in src/config-paths.ts (getGlobalConfigPath, getProjectConfigPath, etc.) — the natural home for an agents-directory helper. pi-subagents encodes the same project convention independently in config/custom-agents.ts (join(cwd, ".pi", "agents")); the global convention (join(getAgentDir(), "agents")) is also duplicated between the two packages and has never been a coupling problem, because both are honoring the same platform contract rather than depending on each other.
The SDK (@earendil-works/pi-coding-agent) exposes getAgentDir() — the single Pi agent's home — but no agents-directory helper, because Pi is single-agent and has no concept of multiple named agents (see the next subsection and Open Questions).
So independent encoding plus a test is the pragmatic, decoupling-preserving answer.
Per-agent frontmatter is a multi-agent integration concern, not a core one
Pi is single-agent by deliberate design — it has no concept of multiple named agents.
The notion of agent types is introduced entirely by external extensions (pi-subagents, pi-agent-router, some MasuRii packages); /agents is itself a pi-subagents command. pi-permission-system already reflects this: it learns the active agent's name from a generic <active_agent name="..."> tag injected into the system prompt (by pi-agent-router) or an active_agent session entry — never from a hard dependency on any one multi-agent extension.
It does not enumerate or discover agents (pi-subagents owns that); it reads only the permission: sub-document of an agent file, by the active agent's name, on demand.
Because pps bridges to multi-agent tooling through generic, extension-agnostic signals rather than a pi-subagents dependency, it likewise reads the agent file itself rather than relying on any one extension to push the data — so encoding the <cwd>/.pi/agents convention is, for now, pps's own integration-layer responsibility.
See Open Questions for the longer-term direction.
Design Overview
Add a convention helper to src/config-paths.ts:
/**
* Directory holding project-scoped custom agent definitions.
*
* `<cwd>/.pi/agents` is a Pi platform convention, also encoded by
* @gotgenes/pi-subagents' `loadCustomAgents` (`config/custom-agents.ts`).
* The two packages encode it independently — pi-permission-system has no
* dependency on pi-subagents (ADR-0002) — so this is this package's
* authoritative copy.
*/
export function getProjectAgentsDir(cwd: string): string {
return join(cwd, ".pi", "agents");
}
derivePolicyLoaderOptions then calls it:
projectAgentsDir: cwd ? getProjectAgentsDir(cwd) : undefined,
No data shape changes — PolicyLoaderOptions, ResolvedPolicyPaths, and the merge precedence are untouched.
The only observable difference is the resolved path value, which now points at the correct directory.
Edge cases
cwdabsent (global-only) —projectAgentsDirstaysundefined; unchanged.- A project agent file present at the new path but absent at the old one — now found and applied (the bug's scenario).
- Cache stamping (
getCacheStamp) already keys offthis.projectAgentsDir, so correcting the directory makes change-detection track the right file with no further work.
Module-Level Changes
src/config-paths.ts— addgetProjectAgentsDir(cwd)with the convention comment.src/permission-manager.ts— importgetProjectAgentsDir; replace the inlinejoin(cwd, ".pi", "agent", "agents")inderivePolicyLoaderOptionswithgetProjectAgentsDir(cwd).test/config-paths.test.ts— add a unit test forgetProjectAgentsDir.test/permission-manager-unified.test.ts— extend themakeAgentDirSetuphelper (in theconfigureForCwd and agentDir optiondescribe block) to optionally write a project agent.md; add a path-level assertion and a behavior-level regression test.docs/configuration.md— under "Project Agent Override", change the path from<cwd>/.pi/agent/agents/<agent>.mdto<cwd>/.pi/agents/<agent>.md.
Searched for other affected references:
- The
package-pi-permission-systemSKILL.md documents the per-agent override mechanism without stating the project path, so it needs no change. README.mdreferences per-agent overrides without a path; no change.test/config-reporter.test.tsuses/projects/my-app/.pi/agent/agentsas an arbitrary sample string fed to the reporter (not a derived value); leaving it is harmless — an optional cosmetic update only (see Open Questions).
Test Impact Analysis
- New tests enabled by the fix:
- A unit test for the pure
getProjectAgentsDirhelper. - A regression test that drives the real
agentDir-based derivation throughconfigureForCwd(cwd)and asserts a project agent'spermission:frontmatter is enforced — previously impossible because every existing test passedprojectAgentsDirexplicitly and so never exercised the buggy derivation.
- A unit test for the pure
- Redundant tests: none.
The existing
getResolvedPolicyPathstests pass explicit paths and exercise reporting, not derivation. - Tests that must stay as-is: the explicit-path
getResolvedPolicyPathstests and the existingconfigureForCwdprecedence tests, which genuinely exercise the loader rebuild and merge precedence.
Invariants at risk
- The global agents directory derivation (
join(agentDir, "agents")) must stay unchanged. It is pinned by the existingconstruction with { agentDir } reads global config from getGlobalConfigPath(agentDir)test intest/permission-manager-unified.test.ts; the new path-level assertion will additionally confirmagentsDiris unaffected alongside the correctedprojectAgentsDir. - The project/global config-path derivations (
getProjectConfigPath,getGlobalConfigPath) must stay unchanged; the existing precedence tests in the same describe block pin them.
TDD Order
fix!: correct the project agents directory resolution.- Add
getProjectAgentsDir(cwd)tosrc/config-paths.ts. - Add its unit test to
test/config-paths.test.ts. - Extend
makeAgentDirSetupto optionally write a project agent file, then add to theconfigureForCwd and agentDir optiondescribe block: (a) a path-level test assertinggetResolvedPolicyPaths().projectAgentsDir === join(cwd, ".pi", "agents")(and thatagentsDiris unchanged), and (b) a behavior-level test that writes<cwd>/.pi/agents/coder.mdwithpermission:\n read: deny, callsconfigureForCwd(cwd), and assertscheckPermission("read", { path: "foo.txt" }, "coder").state === "deny". These fail against the current code. - Wire
getProjectAgentsDirintoderivePolicyLoaderOptionsto make them pass. - Run
pnpm --filter @gotgenes/pi-permission-system exec vitest run(the helper change touches shared resolution). - Commit (
fix!:) with aBREAKING CHANGE:footer: project agents'permission:frontmatter at<cwd>/.pi/agents/<name>.mdis now read and enforced; previously the wrong directory was checked and the frontmatter was silently ignored, so a session may become more restrictive on upgrade. Remediation: if a project agent'spermission:block restricts tools unexpectedly, edit or remove that block in<cwd>/.pi/agents/<name>.md. Reference the issue as(#428)in the subject.
- Add
docs: correct the documented project-agent override path indocs/configuration.mdfrom<cwd>/.pi/agent/agents/<agent>.mdto<cwd>/.pi/agents/<agent>.md. Commit (docs:), referencing(#428).
Risks and Mitigations
- Behavior change on upgrade (sessions may become more restrictive when a project agent declares
permission:). Mitigated by thefix!:classification, theBREAKING CHANGE:footer with remediation, and the corrected documentation;/ship-issuesurfaces the same summary in the close comment. - Drift between this package's and pi-subagents' encoding of the convention.
Mitigated by the cross-reference comment on
getProjectAgentsDirand the behavior-level regression test pinning the resolved path. - Cache-key correctness:
getCacheStampalready keys offprojectAgentsDir, so correcting the directory only improves change detection — no regression risk.
Open Questions
- Long-term, per-agent
permission:frontmatter is best modeled as an extension bridge on top of pps's single-agent core, not a core responsibility. Pi is single-agent by deliberate design, so neither the SDK nor a hypothetical "small core" should own an agents directory or parse agent frontmatter — doing so would push a multi-agent concept into a core that rejects it. (Earlier drafts of this plan suggested upstreaminggetProjectAgentsDirto the SDK and having the core parse agent frontmatter; both are withdrawn for this reason.) - A cleaner evolution keeps the bridge generic, mirroring how pps already consumes the active-agent signal: the multi-agent extension that owns agent definitions (and already parses them) would supply the active agent's
permission:overrides to pps through an extension-agnostic channel, so pps's core never locates or parses agent files. Until such a channel exists, pps encodes the<cwd>/.pi/agentsconvention itself — which is what this fix does. - Optional symmetry: extract
getGlobalAgentsDir(agentDir)and dedupejoin(agentDir, "agents")acrossderivePolicyLoaderOptionsanddefaultAgentsDir(). Deferred — the global path is not buggy, and touching it widens the blast radius.