mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-20 18:14:29 -07:00
refactor(policy): rename "Project" policies to "Workspace" policies
Updates the terminology and configuration for the intermediate policy tier from "Project" to "Workspace" to better align with the Gemini CLI ecosystem. Key changes: - Renamed `PROJECT_POLICY_TIER` to `WORKSPACE_POLICY_TIER`. - Renamed `getProjectPoliciesDir` to `getWorkspacePoliciesDir`. - Updated integrity scope from `project` to `workspace`. - Updated UI dialogs and documentation. - Renamed related test files.
This commit is contained in:
+15
-15
@@ -92,12 +92,12 @@ rule with the highest priority wins**.
|
|||||||
To provide a clear hierarchy, policies are organized into three tiers. Each tier
|
To provide a clear hierarchy, policies are organized into three tiers. Each tier
|
||||||
has a designated number that forms the base of the final priority calculation.
|
has a designated number that forms the base of the final priority calculation.
|
||||||
|
|
||||||
| Tier | Base | Description |
|
| Tier | Base | Description |
|
||||||
| :------ | :--- | :------------------------------------------------------------------------- |
|
| :-------- | :--- | :------------------------------------------------------------------------- |
|
||||||
| Default | 1 | Built-in policies that ship with the Gemini CLI. |
|
| Default | 1 | Built-in policies that ship with the Gemini CLI. |
|
||||||
| Project | 2 | Policies defined in the current project's configuration directory. |
|
| Workspace | 2 | Policies defined in the current workspace's configuration directory. |
|
||||||
| User | 3 | Custom policies defined by the user. |
|
| User | 3 | Custom policies defined by the user. |
|
||||||
| Admin | 4 | Policies managed by an administrator (e.g., in an enterprise environment). |
|
| Admin | 4 | Policies managed by an administrator (e.g., in an enterprise environment). |
|
||||||
|
|
||||||
Within a TOML policy file, you assign a priority value from **0 to 999**. The
|
Within a TOML policy file, you assign a priority value from **0 to 999**. The
|
||||||
engine transforms this into a final priority using the following formula:
|
engine transforms this into a final priority using the following formula:
|
||||||
@@ -106,15 +106,15 @@ engine transforms this into a final priority using the following formula:
|
|||||||
|
|
||||||
This system guarantees that:
|
This system guarantees that:
|
||||||
|
|
||||||
- Admin policies always override User, Project, and Default policies.
|
- Admin policies always override User, Workspace, and Default policies.
|
||||||
- User policies override Project and Default policies.
|
- User policies override Workspace and Default policies.
|
||||||
- Project policies override Default policies.
|
- Workspace policies override Default policies.
|
||||||
- You can still order rules within a single tier with fine-grained control.
|
- You can still order rules within a single tier with fine-grained control.
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
- A `priority: 50` rule in a Default policy file becomes `1.050`.
|
- A `priority: 50` rule in a Default policy file becomes `1.050`.
|
||||||
- A `priority: 10` rule in a Project policy file becomes `2.010`.
|
- A `priority: 10` rule in a Workspace policy policy file becomes `2.010`.
|
||||||
- A `priority: 100` rule in a User policy file becomes `3.100`.
|
- A `priority: 100` rule in a User policy file becomes `3.100`.
|
||||||
- A `priority: 20` rule in an Admin policy file becomes `4.020`.
|
- A `priority: 20` rule in an Admin policy file becomes `4.020`.
|
||||||
|
|
||||||
@@ -159,11 +159,11 @@ User, and (if configured) Admin directories.
|
|||||||
|
|
||||||
### Policy locations
|
### Policy locations
|
||||||
|
|
||||||
| Tier | Type | Location |
|
| Tier | Type | Location |
|
||||||
| :---------- | :----- | :-------------------------------------- |
|
| :------------ | :----- | :---------------------------------------- |
|
||||||
| **User** | Custom | `~/.gemini/policies/*.toml` |
|
| **User** | Custom | `~/.gemini/policies/*.toml` |
|
||||||
| **Project** | Custom | `$PROJECT_ROOT/.gemini/policies/*.toml` |
|
| **Workspace** | Custom | `$WORKSPACE_ROOT/.gemini/policies/*.toml` |
|
||||||
| **Admin** | System | _See below (OS specific)_ |
|
| **Admin** | System | _See below (OS specific)_ |
|
||||||
|
|
||||||
#### System-wide policies (Admin)
|
#### System-wide policies (Admin)
|
||||||
|
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ export async function parseArguments(
|
|||||||
.option('accept-changed-policies', {
|
.option('accept-changed-policies', {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
description:
|
description:
|
||||||
'Automatically accept changed project policies (use with caution).',
|
'Automatically accept changed workspace policies (use with caution).',
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
// Register MCP subcommands
|
// Register MCP subcommands
|
||||||
@@ -702,52 +702,52 @@ export async function loadCliConfig(
|
|||||||
policyPaths: argv.policy,
|
policyPaths: argv.policy,
|
||||||
};
|
};
|
||||||
|
|
||||||
let projectPoliciesDir: string | undefined;
|
let workspacePoliciesDir: string | undefined;
|
||||||
let policyUpdateConfirmationRequest:
|
let policyUpdateConfirmationRequest:
|
||||||
| PolicyUpdateConfirmationRequest
|
| PolicyUpdateConfirmationRequest
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
if (trustedFolder) {
|
if (trustedFolder) {
|
||||||
const potentialProjectPoliciesDir = new Storage(
|
const potentialWorkspacePoliciesDir = new Storage(
|
||||||
cwd,
|
cwd,
|
||||||
).getProjectPoliciesDir();
|
).getWorkspacePoliciesDir();
|
||||||
const integrityManager = new PolicyIntegrityManager();
|
const integrityManager = new PolicyIntegrityManager();
|
||||||
const integrityResult = await integrityManager.checkIntegrity(
|
const integrityResult = await integrityManager.checkIntegrity(
|
||||||
'project',
|
'workspace',
|
||||||
cwd,
|
cwd,
|
||||||
potentialProjectPoliciesDir,
|
potentialWorkspacePoliciesDir,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (integrityResult.status === IntegrityStatus.MATCH) {
|
if (integrityResult.status === IntegrityStatus.MATCH) {
|
||||||
projectPoliciesDir = potentialProjectPoliciesDir;
|
workspacePoliciesDir = potentialWorkspacePoliciesDir;
|
||||||
} else if (
|
} else if (
|
||||||
integrityResult.status === IntegrityStatus.NEW &&
|
integrityResult.status === IntegrityStatus.NEW &&
|
||||||
integrityResult.fileCount === 0
|
integrityResult.fileCount === 0
|
||||||
) {
|
) {
|
||||||
// No project policies found
|
// No workspace policies found
|
||||||
projectPoliciesDir = undefined;
|
workspacePoliciesDir = undefined;
|
||||||
} else {
|
} else {
|
||||||
// Policies changed or are new
|
// Policies changed or are new
|
||||||
if (argv.acceptChangedPolicies) {
|
if (argv.acceptChangedPolicies) {
|
||||||
debugLogger.warn(
|
debugLogger.warn(
|
||||||
'WARNING: Project policies changed or are new. Auto-accepting due to --accept-changed-policies flag.',
|
'WARNING: Workspace policies changed or are new. Auto-accepting due to --accept-changed-policies flag.',
|
||||||
);
|
);
|
||||||
await integrityManager.acceptIntegrity(
|
await integrityManager.acceptIntegrity(
|
||||||
'project',
|
'workspace',
|
||||||
cwd,
|
cwd,
|
||||||
integrityResult.hash,
|
integrityResult.hash,
|
||||||
);
|
);
|
||||||
projectPoliciesDir = potentialProjectPoliciesDir;
|
workspacePoliciesDir = potentialWorkspacePoliciesDir;
|
||||||
} else if (interactive) {
|
} else if (interactive) {
|
||||||
policyUpdateConfirmationRequest = {
|
policyUpdateConfirmationRequest = {
|
||||||
scope: 'project',
|
scope: 'workspace',
|
||||||
identifier: cwd,
|
identifier: cwd,
|
||||||
policyDir: potentialProjectPoliciesDir,
|
policyDir: potentialWorkspacePoliciesDir,
|
||||||
newHash: integrityResult.hash,
|
newHash: integrityResult.hash,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
debugLogger.warn(
|
debugLogger.warn(
|
||||||
'WARNING: Project policies changed or are new. Loading default policies only. Use --accept-changed-policies to accept.',
|
'WARNING: Workspace policies changed or are new. Loading default policies only. Use --accept-changed-policies to accept.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -756,7 +756,7 @@ export async function loadCliConfig(
|
|||||||
const policyEngineConfig = await createPolicyEngineConfig(
|
const policyEngineConfig = await createPolicyEngineConfig(
|
||||||
effectiveSettings,
|
effectiveSettings,
|
||||||
approvalMode,
|
approvalMode,
|
||||||
projectPoliciesDir,
|
workspacePoliciesDir,
|
||||||
);
|
);
|
||||||
policyEngineConfig.nonInteractive = !interactive;
|
policyEngineConfig.nonInteractive = !interactive;
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { type Settings } from './settings.js';
|
|||||||
export async function createPolicyEngineConfig(
|
export async function createPolicyEngineConfig(
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
approvalMode: ApprovalMode,
|
approvalMode: ApprovalMode,
|
||||||
projectPoliciesDir?: string,
|
workspacePoliciesDir?: string,
|
||||||
): Promise<PolicyEngineConfig> {
|
): Promise<PolicyEngineConfig> {
|
||||||
// Explicitly construct PolicySettings from Settings to ensure type safety
|
// Explicitly construct PolicySettings from Settings to ensure type safety
|
||||||
// and avoid accidental leakage of other settings properties.
|
// and avoid accidental leakage of other settings properties.
|
||||||
@@ -33,7 +33,7 @@ export async function createPolicyEngineConfig(
|
|||||||
policySettings,
|
policySettings,
|
||||||
approvalMode,
|
approvalMode,
|
||||||
undefined,
|
undefined,
|
||||||
projectPoliciesDir,
|
workspacePoliciesDir,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-12
@@ -49,7 +49,7 @@ vi.mock('@google/gemini-cli-core', async () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Project-Level Policy CLI Integration', () => {
|
describe('Workspace-Level Policy CLI Integration', () => {
|
||||||
const MOCK_CWD = process.cwd();
|
const MOCK_CWD = process.cwd();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -63,13 +63,13 @@ describe('Project-Level Policy CLI Integration', () => {
|
|||||||
vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(false);
|
vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have getProjectPoliciesDir on Storage class', () => {
|
it('should have getWorkspacePoliciesDir on Storage class', () => {
|
||||||
const storage = new ServerConfig.Storage(MOCK_CWD);
|
const storage = new ServerConfig.Storage(MOCK_CWD);
|
||||||
expect(storage.getProjectPoliciesDir).toBeDefined();
|
expect(storage.getWorkspacePoliciesDir).toBeDefined();
|
||||||
expect(typeof storage.getProjectPoliciesDir).toBe('function');
|
expect(typeof storage.getWorkspacePoliciesDir).toBe('function');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should pass projectPoliciesDir to createPolicyEngineConfig when folder is trusted', async () => {
|
it('should pass workspacePoliciesDir to createPolicyEngineConfig when folder is trusted', async () => {
|
||||||
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
||||||
isTrusted: true,
|
isTrusted: true,
|
||||||
source: 'file',
|
source: 'file',
|
||||||
@@ -88,7 +88,7 @@ describe('Project-Level Policy CLI Integration', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should NOT pass projectPoliciesDir to createPolicyEngineConfig when folder is NOT trusted', async () => {
|
it('should NOT pass workspacePoliciesDir to createPolicyEngineConfig when folder is NOT trusted', async () => {
|
||||||
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
||||||
isTrusted: false,
|
isTrusted: false,
|
||||||
source: 'file',
|
source: 'file',
|
||||||
@@ -107,7 +107,7 @@ describe('Project-Level Policy CLI Integration', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should NOT pass projectPoliciesDir if integrity is NEW but fileCount is 0', async () => {
|
it('should NOT pass workspacePoliciesDir if integrity is NEW but fileCount is 0', async () => {
|
||||||
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
||||||
isTrusted: true,
|
isTrusted: true,
|
||||||
source: 'file',
|
source: 'file',
|
||||||
@@ -131,7 +131,7 @@ describe('Project-Level Policy CLI Integration', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should warn and NOT pass projectPoliciesDir if integrity MISMATCH in non-interactive mode', async () => {
|
it('should warn and NOT pass workspacePoliciesDir if integrity MISMATCH in non-interactive mode', async () => {
|
||||||
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
||||||
isTrusted: true,
|
isTrusted: true,
|
||||||
source: 'file',
|
source: 'file',
|
||||||
@@ -149,7 +149,7 @@ describe('Project-Level Policy CLI Integration', () => {
|
|||||||
await loadCliConfig(settings, 'test-session', argv, { cwd: MOCK_CWD });
|
await loadCliConfig(settings, 'test-session', argv, { cwd: MOCK_CWD });
|
||||||
|
|
||||||
expect(debugLogger.warn).toHaveBeenCalledWith(
|
expect(debugLogger.warn).toHaveBeenCalledWith(
|
||||||
expect.stringContaining('Project policies changed or are new'),
|
expect.stringContaining('Workspace policies changed or are new'),
|
||||||
);
|
);
|
||||||
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
|
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
@@ -176,7 +176,7 @@ describe('Project-Level Policy CLI Integration', () => {
|
|||||||
await loadCliConfig(settings, 'test-session', argv, { cwd: MOCK_CWD });
|
await loadCliConfig(settings, 'test-session', argv, { cwd: MOCK_CWD });
|
||||||
|
|
||||||
expect(mockAcceptIntegrity).toHaveBeenCalledWith(
|
expect(mockAcceptIntegrity).toHaveBeenCalledWith(
|
||||||
'project',
|
'workspace',
|
||||||
MOCK_CWD,
|
MOCK_CWD,
|
||||||
'new-hash',
|
'new-hash',
|
||||||
);
|
);
|
||||||
@@ -211,7 +211,7 @@ describe('Project-Level Policy CLI Integration', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(config.getPolicyUpdateConfirmationRequest()).toEqual({
|
expect(config.getPolicyUpdateConfirmationRequest()).toEqual({
|
||||||
scope: 'project',
|
scope: 'workspace',
|
||||||
identifier: MOCK_CWD,
|
identifier: MOCK_CWD,
|
||||||
policyDir: expect.stringContaining(path.join('.gemini', 'policies')),
|
policyDir: expect.stringContaining(path.join('.gemini', 'policies')),
|
||||||
newHash: 'new-hash',
|
newHash: 'new-hash',
|
||||||
@@ -247,7 +247,7 @@ describe('Project-Level Policy CLI Integration', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(config.getPolicyUpdateConfirmationRequest()).toEqual({
|
expect(config.getPolicyUpdateConfirmationRequest()).toEqual({
|
||||||
scope: 'project',
|
scope: 'workspace',
|
||||||
identifier: MOCK_CWD,
|
identifier: MOCK_CWD,
|
||||||
policyDir: expect.stringContaining(path.join('.gemini', 'policies')),
|
policyDir: expect.stringContaining(path.join('.gemini', 'policies')),
|
||||||
newHash: 'new-hash',
|
newHash: 'new-hash',
|
||||||
@@ -23,14 +23,14 @@ describe('PolicyUpdateDialog', () => {
|
|||||||
const { lastFrame } = renderWithProviders(
|
const { lastFrame } = renderWithProviders(
|
||||||
<PolicyUpdateDialog
|
<PolicyUpdateDialog
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
scope="project"
|
scope="workspace"
|
||||||
identifier="/test/path"
|
identifier="/test/path"
|
||||||
isRestarting={false}
|
isRestarting={false}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const output = lastFrame();
|
const output = lastFrame();
|
||||||
expect(output).toContain('New or changed project policies detected');
|
expect(output).toContain('New or changed workspace policies detected');
|
||||||
expect(output).toContain('Location: /test/path');
|
expect(output).toContain('Location: /test/path');
|
||||||
expect(output).toContain('Accept and Load');
|
expect(output).toContain('Accept and Load');
|
||||||
expect(output).toContain('Ignore');
|
expect(output).toContain('Ignore');
|
||||||
@@ -41,7 +41,7 @@ describe('PolicyUpdateDialog', () => {
|
|||||||
const { stdin } = renderWithProviders(
|
const { stdin } = renderWithProviders(
|
||||||
<PolicyUpdateDialog
|
<PolicyUpdateDialog
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
scope="project"
|
scope="workspace"
|
||||||
identifier="/test/path"
|
identifier="/test/path"
|
||||||
isRestarting={false}
|
isRestarting={false}
|
||||||
/>,
|
/>,
|
||||||
@@ -62,7 +62,7 @@ describe('PolicyUpdateDialog', () => {
|
|||||||
const { stdin } = renderWithProviders(
|
const { stdin } = renderWithProviders(
|
||||||
<PolicyUpdateDialog
|
<PolicyUpdateDialog
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
scope="project"
|
scope="workspace"
|
||||||
identifier="/test/path"
|
identifier="/test/path"
|
||||||
isRestarting={false}
|
isRestarting={false}
|
||||||
/>,
|
/>,
|
||||||
@@ -86,7 +86,7 @@ describe('PolicyUpdateDialog', () => {
|
|||||||
const { stdin } = renderWithProviders(
|
const { stdin } = renderWithProviders(
|
||||||
<PolicyUpdateDialog
|
<PolicyUpdateDialog
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
scope="project"
|
scope="workspace"
|
||||||
identifier="/test/path"
|
identifier="/test/path"
|
||||||
isRestarting={false}
|
isRestarting={false}
|
||||||
/>,
|
/>,
|
||||||
@@ -106,7 +106,7 @@ describe('PolicyUpdateDialog', () => {
|
|||||||
const { lastFrame } = renderWithProviders(
|
const { lastFrame } = renderWithProviders(
|
||||||
<PolicyUpdateDialog
|
<PolicyUpdateDialog
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
scope="project"
|
scope="workspace"
|
||||||
identifier="/test/path"
|
identifier="/test/path"
|
||||||
isRestarting={true}
|
isRestarting={true}
|
||||||
/>,
|
/>,
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ export class Storage {
|
|||||||
return path.join(tempDir, identifier);
|
return path.join(tempDir, identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
getProjectPoliciesDir(): string {
|
getWorkspacePoliciesDir(): string {
|
||||||
return path.join(this.getGeminiDir(), 'policies');
|
return path.join(this.getGeminiDir(), 'policies');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export const DEFAULT_CORE_POLICIES_DIR = path.join(__dirname, 'policies');
|
|||||||
|
|
||||||
// Policy tier constants for priority calculation
|
// Policy tier constants for priority calculation
|
||||||
export const DEFAULT_POLICY_TIER = 1;
|
export const DEFAULT_POLICY_TIER = 1;
|
||||||
export const PROJECT_POLICY_TIER = 2;
|
export const WORKSPACE_POLICY_TIER = 2;
|
||||||
export const USER_POLICY_TIER = 3;
|
export const USER_POLICY_TIER = 3;
|
||||||
export const ADMIN_POLICY_TIER = 4;
|
export const ADMIN_POLICY_TIER = 4;
|
||||||
|
|
||||||
@@ -49,12 +49,12 @@ export const ADMIN_POLICY_TIER = 4;
|
|||||||
* @param defaultPoliciesDir Optional path to a directory containing default policies.
|
* @param defaultPoliciesDir Optional path to a directory containing default policies.
|
||||||
* @param policyPaths Optional user-provided policy paths (from --policy flag).
|
* @param policyPaths Optional user-provided policy paths (from --policy flag).
|
||||||
* When provided, these replace the default user policies directory.
|
* When provided, these replace the default user policies directory.
|
||||||
* @param projectPoliciesDir Optional path to a directory containing project policies.
|
* @param workspacePoliciesDir Optional path to a directory containing workspace policies.
|
||||||
*/
|
*/
|
||||||
export function getPolicyDirectories(
|
export function getPolicyDirectories(
|
||||||
defaultPoliciesDir?: string,
|
defaultPoliciesDir?: string,
|
||||||
policyPaths?: string[],
|
policyPaths?: string[],
|
||||||
projectPoliciesDir?: string,
|
workspacePoliciesDir?: string,
|
||||||
): string[] {
|
): string[] {
|
||||||
const dirs = [];
|
const dirs = [];
|
||||||
|
|
||||||
@@ -68,9 +68,9 @@ export function getPolicyDirectories(
|
|||||||
dirs.push(Storage.getUserPoliciesDir());
|
dirs.push(Storage.getUserPoliciesDir());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Project Tier (third highest)
|
// Workspace Tier (third highest)
|
||||||
if (projectPoliciesDir) {
|
if (workspacePoliciesDir) {
|
||||||
dirs.push(projectPoliciesDir);
|
dirs.push(workspacePoliciesDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default tier (lowest priority)
|
// Default tier (lowest priority)
|
||||||
@@ -80,13 +80,13 @@ export function getPolicyDirectories(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines the policy tier (1=default, 2=user, 3=project, 4=admin) for a given directory.
|
* Determines the policy tier (1=default, 2=user, 3=workspace, 4=admin) for a given directory.
|
||||||
* This is used by the TOML loader to assign priority bands.
|
* This is used by the TOML loader to assign priority bands.
|
||||||
*/
|
*/
|
||||||
export function getPolicyTier(
|
export function getPolicyTier(
|
||||||
dir: string,
|
dir: string,
|
||||||
defaultPoliciesDir?: string,
|
defaultPoliciesDir?: string,
|
||||||
projectPoliciesDir?: string,
|
workspacePoliciesDir?: string,
|
||||||
): number {
|
): number {
|
||||||
const USER_POLICIES_DIR = Storage.getUserPoliciesDir();
|
const USER_POLICIES_DIR = Storage.getUserPoliciesDir();
|
||||||
const ADMIN_POLICIES_DIR = Storage.getSystemPoliciesDir();
|
const ADMIN_POLICIES_DIR = Storage.getSystemPoliciesDir();
|
||||||
@@ -108,10 +108,10 @@ export function getPolicyTier(
|
|||||||
return USER_POLICY_TIER;
|
return USER_POLICY_TIER;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
projectPoliciesDir &&
|
workspacePoliciesDir &&
|
||||||
normalizedDir === path.resolve(projectPoliciesDir)
|
normalizedDir === path.resolve(workspacePoliciesDir)
|
||||||
) {
|
) {
|
||||||
return PROJECT_POLICY_TIER;
|
return WORKSPACE_POLICY_TIER;
|
||||||
}
|
}
|
||||||
if (normalizedDir === normalizedAdmin) {
|
if (normalizedDir === normalizedAdmin) {
|
||||||
return ADMIN_POLICY_TIER;
|
return ADMIN_POLICY_TIER;
|
||||||
@@ -167,12 +167,12 @@ export async function createPolicyEngineConfig(
|
|||||||
settings: PolicySettings,
|
settings: PolicySettings,
|
||||||
approvalMode: ApprovalMode,
|
approvalMode: ApprovalMode,
|
||||||
defaultPoliciesDir?: string,
|
defaultPoliciesDir?: string,
|
||||||
projectPoliciesDir?: string,
|
workspacePoliciesDir?: string,
|
||||||
): Promise<PolicyEngineConfig> {
|
): Promise<PolicyEngineConfig> {
|
||||||
const policyDirs = getPolicyDirectories(
|
const policyDirs = getPolicyDirectories(
|
||||||
defaultPoliciesDir,
|
defaultPoliciesDir,
|
||||||
settings.policyPaths,
|
settings.policyPaths,
|
||||||
projectPoliciesDir,
|
workspacePoliciesDir,
|
||||||
);
|
);
|
||||||
const securePolicyDirs = await filterSecurePolicyDirectories(policyDirs);
|
const securePolicyDirs = await filterSecurePolicyDirectories(policyDirs);
|
||||||
|
|
||||||
@@ -186,7 +186,7 @@ export async function createPolicyEngineConfig(
|
|||||||
checkers: tomlCheckers,
|
checkers: tomlCheckers,
|
||||||
errors,
|
errors,
|
||||||
} = await loadPoliciesFromToml(securePolicyDirs, (p) => {
|
} = await loadPoliciesFromToml(securePolicyDirs, (p) => {
|
||||||
const tier = getPolicyTier(p, defaultPoliciesDir, projectPoliciesDir);
|
const tier = getPolicyTier(p, defaultPoliciesDir, workspacePoliciesDir);
|
||||||
|
|
||||||
// If it's a user-provided path that isn't already categorized as ADMIN,
|
// If it's a user-provided path that isn't already categorized as ADMIN,
|
||||||
// treat it as USER tier.
|
// treat it as USER tier.
|
||||||
@@ -222,11 +222,11 @@ export async function createPolicyEngineConfig(
|
|||||||
//
|
//
|
||||||
// Priority bands (tiers):
|
// Priority bands (tiers):
|
||||||
// - Default policies (TOML): 1 + priority/1000 (e.g., priority 100 → 1.100)
|
// - Default policies (TOML): 1 + priority/1000 (e.g., priority 100 → 1.100)
|
||||||
// - Project policies (TOML): 2 + priority/1000 (e.g., priority 100 → 2.100)
|
// - Workspace policies (TOML): 2 + priority/1000 (e.g., priority 100 → 2.100)
|
||||||
// - User policies (TOML): 3 + priority/1000 (e.g., priority 100 → 3.100)
|
// - User policies (TOML): 3 + priority/1000 (e.g., priority 100 → 3.100)
|
||||||
// - Admin policies (TOML): 4 + priority/1000 (e.g., priority 100 → 4.100)
|
// - Admin policies (TOML): 4 + priority/1000 (e.g., priority 100 → 4.100)
|
||||||
//
|
//
|
||||||
// This ensures Admin > User > Project > Default hierarchy is always preserved,
|
// This ensures Admin > User > Workspace > Default hierarchy is always preserved,
|
||||||
// while allowing user-specified priorities to work within each tier.
|
// while allowing user-specified priorities to work within each tier.
|
||||||
//
|
//
|
||||||
// Settings-based and dynamic rules (all in user tier 3.x):
|
// Settings-based and dynamic rules (all in user tier 3.x):
|
||||||
|
|||||||
@@ -61,11 +61,11 @@ describe('PolicyIntegrityManager', () => {
|
|||||||
it('should return NEW if no stored hash', async () => {
|
it('should return NEW if no stored hash', async () => {
|
||||||
mockFs.readFile.mockRejectedValue({ code: 'ENOENT' }); // No stored file
|
mockFs.readFile.mockRejectedValue({ code: 'ENOENT' }); // No stored file
|
||||||
readPolicyFilesMock.mockResolvedValue([
|
readPolicyFilesMock.mockResolvedValue([
|
||||||
{ path: '/project/policies/a.toml', content: 'contentA' },
|
{ path: '/workspace/policies/a.toml', content: 'contentA' },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const result = await integrityManager.checkIntegrity(
|
const result = await integrityManager.checkIntegrity(
|
||||||
'project',
|
'workspace',
|
||||||
'id',
|
'id',
|
||||||
'/dir',
|
'/dir',
|
||||||
);
|
);
|
||||||
@@ -77,13 +77,13 @@ describe('PolicyIntegrityManager', () => {
|
|||||||
|
|
||||||
it('should return MATCH if stored hash matches', async () => {
|
it('should return MATCH if stored hash matches', async () => {
|
||||||
readPolicyFilesMock.mockResolvedValue([
|
readPolicyFilesMock.mockResolvedValue([
|
||||||
{ path: '/project/policies/a.toml', content: 'contentA' },
|
{ path: '/workspace/policies/a.toml', content: 'contentA' },
|
||||||
]);
|
]);
|
||||||
// We can't easily get the expected hash without calling private method or re-implementing logic.
|
// We can't easily get the expected hash without calling private method or re-implementing logic.
|
||||||
// But we can run checkIntegrity once (NEW) to get the hash, then mock FS with that hash.
|
// But we can run checkIntegrity once (NEW) to get the hash, then mock FS with that hash.
|
||||||
mockFs.readFile.mockRejectedValue({ code: 'ENOENT' });
|
mockFs.readFile.mockRejectedValue({ code: 'ENOENT' });
|
||||||
const resultNew = await integrityManager.checkIntegrity(
|
const resultNew = await integrityManager.checkIntegrity(
|
||||||
'project',
|
'workspace',
|
||||||
'id',
|
'id',
|
||||||
'/dir',
|
'/dir',
|
||||||
);
|
);
|
||||||
@@ -91,12 +91,12 @@ describe('PolicyIntegrityManager', () => {
|
|||||||
|
|
||||||
mockFs.readFile.mockResolvedValue(
|
mockFs.readFile.mockResolvedValue(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
'project:id': currentHash,
|
'workspace:id': currentHash,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await integrityManager.checkIntegrity(
|
const result = await integrityManager.checkIntegrity(
|
||||||
'project',
|
'workspace',
|
||||||
'id',
|
'id',
|
||||||
'/dir',
|
'/dir',
|
||||||
);
|
);
|
||||||
@@ -108,10 +108,10 @@ describe('PolicyIntegrityManager', () => {
|
|||||||
it('should return MISMATCH if stored hash differs', async () => {
|
it('should return MISMATCH if stored hash differs', async () => {
|
||||||
mockFs.readFile.mockRejectedValue({ code: 'ENOENT' });
|
mockFs.readFile.mockRejectedValue({ code: 'ENOENT' });
|
||||||
readPolicyFilesMock.mockResolvedValue([
|
readPolicyFilesMock.mockResolvedValue([
|
||||||
{ path: '/project/policies/a.toml', content: 'contentA' },
|
{ path: '/workspace/policies/a.toml', content: 'contentA' },
|
||||||
]);
|
]);
|
||||||
const resultNew = await integrityManager.checkIntegrity(
|
const resultNew = await integrityManager.checkIntegrity(
|
||||||
'project',
|
'workspace',
|
||||||
'id',
|
'id',
|
||||||
'/dir',
|
'/dir',
|
||||||
);
|
);
|
||||||
@@ -119,12 +119,12 @@ describe('PolicyIntegrityManager', () => {
|
|||||||
|
|
||||||
mockFs.readFile.mockResolvedValue(
|
mockFs.readFile.mockResolvedValue(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
'project:id': 'different_hash',
|
'workspace:id': 'different_hash',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await integrityManager.checkIntegrity(
|
const result = await integrityManager.checkIntegrity(
|
||||||
'project',
|
'workspace',
|
||||||
'id',
|
'id',
|
||||||
'/dir',
|
'/dir',
|
||||||
);
|
);
|
||||||
@@ -137,21 +137,21 @@ describe('PolicyIntegrityManager', () => {
|
|||||||
mockFs.readFile.mockRejectedValue({ code: 'ENOENT' });
|
mockFs.readFile.mockRejectedValue({ code: 'ENOENT' });
|
||||||
|
|
||||||
readPolicyFilesMock.mockResolvedValue([
|
readPolicyFilesMock.mockResolvedValue([
|
||||||
{ path: '/project/policies/a.toml', content: 'contentA' },
|
{ path: '/workspace/policies/a.toml', content: 'contentA' },
|
||||||
]);
|
]);
|
||||||
const result1 = await integrityManager.checkIntegrity(
|
const result1 = await integrityManager.checkIntegrity(
|
||||||
'project',
|
'workspace',
|
||||||
'id',
|
'id',
|
||||||
'/project/policies',
|
'/workspace/policies',
|
||||||
);
|
);
|
||||||
|
|
||||||
readPolicyFilesMock.mockResolvedValue([
|
readPolicyFilesMock.mockResolvedValue([
|
||||||
{ path: '/project/policies/b.toml', content: 'contentA' },
|
{ path: '/workspace/policies/b.toml', content: 'contentA' },
|
||||||
]);
|
]);
|
||||||
const result2 = await integrityManager.checkIntegrity(
|
const result2 = await integrityManager.checkIntegrity(
|
||||||
'project',
|
'workspace',
|
||||||
'id',
|
'id',
|
||||||
'/project/policies',
|
'/workspace/policies',
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result1.hash).not.toBe(result2.hash);
|
expect(result1.hash).not.toBe(result2.hash);
|
||||||
@@ -161,21 +161,21 @@ describe('PolicyIntegrityManager', () => {
|
|||||||
mockFs.readFile.mockRejectedValue({ code: 'ENOENT' });
|
mockFs.readFile.mockRejectedValue({ code: 'ENOENT' });
|
||||||
|
|
||||||
readPolicyFilesMock.mockResolvedValue([
|
readPolicyFilesMock.mockResolvedValue([
|
||||||
{ path: '/project/policies/a.toml', content: 'contentA' },
|
{ path: '/workspace/policies/a.toml', content: 'contentA' },
|
||||||
]);
|
]);
|
||||||
const result1 = await integrityManager.checkIntegrity(
|
const result1 = await integrityManager.checkIntegrity(
|
||||||
'project',
|
'workspace',
|
||||||
'id',
|
'id',
|
||||||
'/project/policies',
|
'/workspace/policies',
|
||||||
);
|
);
|
||||||
|
|
||||||
readPolicyFilesMock.mockResolvedValue([
|
readPolicyFilesMock.mockResolvedValue([
|
||||||
{ path: '/project/policies/a.toml', content: 'contentB' },
|
{ path: '/workspace/policies/a.toml', content: 'contentB' },
|
||||||
]);
|
]);
|
||||||
const result2 = await integrityManager.checkIntegrity(
|
const result2 = await integrityManager.checkIntegrity(
|
||||||
'project',
|
'workspace',
|
||||||
'id',
|
'id',
|
||||||
'/project/policies',
|
'/workspace/policies',
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result1.hash).not.toBe(result2.hash);
|
expect(result1.hash).not.toBe(result2.hash);
|
||||||
@@ -185,23 +185,23 @@ describe('PolicyIntegrityManager', () => {
|
|||||||
mockFs.readFile.mockRejectedValue({ code: 'ENOENT' });
|
mockFs.readFile.mockRejectedValue({ code: 'ENOENT' });
|
||||||
|
|
||||||
readPolicyFilesMock.mockResolvedValue([
|
readPolicyFilesMock.mockResolvedValue([
|
||||||
{ path: '/project/policies/a.toml', content: 'contentA' },
|
{ path: '/workspace/policies/a.toml', content: 'contentA' },
|
||||||
{ path: '/project/policies/b.toml', content: 'contentB' },
|
{ path: '/workspace/policies/b.toml', content: 'contentB' },
|
||||||
]);
|
]);
|
||||||
const result1 = await integrityManager.checkIntegrity(
|
const result1 = await integrityManager.checkIntegrity(
|
||||||
'project',
|
'workspace',
|
||||||
'id',
|
'id',
|
||||||
'/project/policies',
|
'/workspace/policies',
|
||||||
);
|
);
|
||||||
|
|
||||||
readPolicyFilesMock.mockResolvedValue([
|
readPolicyFilesMock.mockResolvedValue([
|
||||||
{ path: '/project/policies/b.toml', content: 'contentB' },
|
{ path: '/workspace/policies/b.toml', content: 'contentB' },
|
||||||
{ path: '/project/policies/a.toml', content: 'contentA' },
|
{ path: '/workspace/policies/a.toml', content: 'contentA' },
|
||||||
]);
|
]);
|
||||||
const result2 = await integrityManager.checkIntegrity(
|
const result2 = await integrityManager.checkIntegrity(
|
||||||
'project',
|
'workspace',
|
||||||
'id',
|
'id',
|
||||||
'/project/policies',
|
'/workspace/policies',
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result1.hash).toBe(result2.hash);
|
expect(result1.hash).toBe(result2.hash);
|
||||||
@@ -215,7 +215,7 @@ describe('PolicyIntegrityManager', () => {
|
|||||||
{ path: '/dirA/p.toml', content: 'contentA' },
|
{ path: '/dirA/p.toml', content: 'contentA' },
|
||||||
]);
|
]);
|
||||||
const { hash: hashA } = await integrityManager.checkIntegrity(
|
const { hash: hashA } = await integrityManager.checkIntegrity(
|
||||||
'project',
|
'workspace',
|
||||||
'idA',
|
'idA',
|
||||||
'/dirA',
|
'/dirA',
|
||||||
);
|
);
|
||||||
@@ -224,7 +224,7 @@ describe('PolicyIntegrityManager', () => {
|
|||||||
{ path: '/dirB/p.toml', content: 'contentB' },
|
{ path: '/dirB/p.toml', content: 'contentB' },
|
||||||
]);
|
]);
|
||||||
const { hash: hashB } = await integrityManager.checkIntegrity(
|
const { hash: hashB } = await integrityManager.checkIntegrity(
|
||||||
'project',
|
'workspace',
|
||||||
'idB',
|
'idB',
|
||||||
'/dirB',
|
'/dirB',
|
||||||
);
|
);
|
||||||
@@ -232,8 +232,8 @@ describe('PolicyIntegrityManager', () => {
|
|||||||
// Now mock storage with both
|
// Now mock storage with both
|
||||||
mockFs.readFile.mockResolvedValue(
|
mockFs.readFile.mockResolvedValue(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
'project:idA': hashA,
|
'workspace:idA': hashA,
|
||||||
'project:idB': 'oldHashB', // Different from hashB
|
'workspace:idB': 'oldHashB', // Different from hashB
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -242,7 +242,7 @@ describe('PolicyIntegrityManager', () => {
|
|||||||
{ path: '/dirA/p.toml', content: 'contentA' },
|
{ path: '/dirA/p.toml', content: 'contentA' },
|
||||||
]);
|
]);
|
||||||
const resultA = await integrityManager.checkIntegrity(
|
const resultA = await integrityManager.checkIntegrity(
|
||||||
'project',
|
'workspace',
|
||||||
'idA',
|
'idA',
|
||||||
'/dirA',
|
'/dirA',
|
||||||
);
|
);
|
||||||
@@ -254,7 +254,7 @@ describe('PolicyIntegrityManager', () => {
|
|||||||
{ path: '/dirB/p.toml', content: 'contentB' },
|
{ path: '/dirB/p.toml', content: 'contentB' },
|
||||||
]);
|
]);
|
||||||
const resultB = await integrityManager.checkIntegrity(
|
const resultB = await integrityManager.checkIntegrity(
|
||||||
'project',
|
'workspace',
|
||||||
'idB',
|
'idB',
|
||||||
'/dirB',
|
'/dirB',
|
||||||
);
|
);
|
||||||
@@ -269,11 +269,11 @@ describe('PolicyIntegrityManager', () => {
|
|||||||
mockFs.mkdir.mockResolvedValue(undefined);
|
mockFs.mkdir.mockResolvedValue(undefined);
|
||||||
mockFs.writeFile.mockResolvedValue(undefined);
|
mockFs.writeFile.mockResolvedValue(undefined);
|
||||||
|
|
||||||
await integrityManager.acceptIntegrity('project', 'id', 'hash123');
|
await integrityManager.acceptIntegrity('workspace', 'id', 'hash123');
|
||||||
|
|
||||||
expect(mockFs.writeFile).toHaveBeenCalledWith(
|
expect(mockFs.writeFile).toHaveBeenCalledWith(
|
||||||
'/mock/storage/policy_integrity.json',
|
'/mock/storage/policy_integrity.json',
|
||||||
JSON.stringify({ 'project:id': 'hash123' }, null, 2),
|
JSON.stringify({ 'workspace:id': 'hash123' }, null, 2),
|
||||||
'utf-8',
|
'utf-8',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -287,14 +287,14 @@ describe('PolicyIntegrityManager', () => {
|
|||||||
mockFs.mkdir.mockResolvedValue(undefined);
|
mockFs.mkdir.mockResolvedValue(undefined);
|
||||||
mockFs.writeFile.mockResolvedValue(undefined);
|
mockFs.writeFile.mockResolvedValue(undefined);
|
||||||
|
|
||||||
await integrityManager.acceptIntegrity('project', 'id', 'hash123');
|
await integrityManager.acceptIntegrity('workspace', 'id', 'hash123');
|
||||||
|
|
||||||
expect(mockFs.writeFile).toHaveBeenCalledWith(
|
expect(mockFs.writeFile).toHaveBeenCalledWith(
|
||||||
'/mock/storage/policy_integrity.json',
|
'/mock/storage/policy_integrity.json',
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
{
|
{
|
||||||
'other:id': 'otherhash',
|
'other:id': 'otherhash',
|
||||||
'project:id': 'hash123',
|
'workspace:id': 'hash123',
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
2,
|
2,
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
#
|
#
|
||||||
# Priority bands (tiers):
|
# Priority bands (tiers):
|
||||||
# - Default policies (TOML): 1 + priority/1000 (e.g., priority 100 → 1.100)
|
# - Default policies (TOML): 1 + priority/1000 (e.g., priority 100 → 1.100)
|
||||||
# - Project policies (TOML): 2 + priority/1000 (e.g., priority 100 → 2.100)
|
# - Workspace policies (TOML): 2 + priority/1000 (e.g., priority 100 → 2.100)
|
||||||
# - User policies (TOML): 3 + priority/1000 (e.g., priority 100 → 3.100)
|
# - User policies (TOML): 3 + priority/1000 (e.g., priority 100 → 3.100)
|
||||||
# - Admin policies (TOML): 4 + priority/1000 (e.g., priority 100 → 4.100)
|
# - Admin policies (TOML): 4 + priority/1000 (e.g., priority 100 → 4.100)
|
||||||
#
|
#
|
||||||
# This ensures Admin > User > Project > Default hierarchy is always preserved,
|
# This ensures Admin > User > Workspace > Default hierarchy is always preserved,
|
||||||
# while allowing user-specified priorities to work within each tier.
|
# while allowing user-specified priorities to work within each tier.
|
||||||
#
|
#
|
||||||
# Settings-based and dynamic rules (all in user tier 3.x):
|
# Settings-based and dynamic rules (all in user tier 3.x):
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
#
|
#
|
||||||
# Priority bands (tiers):
|
# Priority bands (tiers):
|
||||||
# - Default policies (TOML): 1 + priority/1000 (e.g., priority 100 → 1.100)
|
# - Default policies (TOML): 1 + priority/1000 (e.g., priority 100 → 1.100)
|
||||||
# - Project policies (TOML): 2 + priority/1000 (e.g., priority 100 → 2.100)
|
# - Workspace policies (TOML): 2 + priority/1000 (e.g., priority 100 → 2.100)
|
||||||
# - User policies (TOML): 3 + priority/1000 (e.g., priority 100 → 3.100)
|
# - User policies (TOML): 3 + priority/1000 (e.g., priority 100 → 3.100)
|
||||||
# - Admin policies (TOML): 4 + priority/1000 (e.g., priority 100 → 4.100)
|
# - Admin policies (TOML): 4 + priority/1000 (e.g., priority 100 → 4.100)
|
||||||
#
|
#
|
||||||
# This ensures Admin > User > Project > Default hierarchy is always preserved,
|
# This ensures Admin > User > Workspace > Default hierarchy is always preserved,
|
||||||
# while allowing user-specified priorities to work within each tier.
|
# while allowing user-specified priorities to work within each tier.
|
||||||
#
|
#
|
||||||
# Settings-based and dynamic rules (all in user tier 3.x):
|
# Settings-based and dynamic rules (all in user tier 3.x):
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
#
|
#
|
||||||
# Priority bands (tiers):
|
# Priority bands (tiers):
|
||||||
# - Default policies (TOML): 1 + priority/1000 (e.g., priority 100 → 1.100)
|
# - Default policies (TOML): 1 + priority/1000 (e.g., priority 100 → 1.100)
|
||||||
# - Project policies (TOML): 2 + priority/1000 (e.g., priority 100 → 2.100)
|
# - Workspace policies (TOML): 2 + priority/1000 (e.g., priority 100 → 2.100)
|
||||||
# - User policies (TOML): 3 + priority/1000 (e.g., priority 100 → 3.100)
|
# - User policies (TOML): 3 + priority/1000 (e.g., priority 100 → 3.100)
|
||||||
# - Admin policies (TOML): 4 + priority/1000 (e.g., priority 100 → 4.100)
|
# - Admin policies (TOML): 4 + priority/1000 (e.g., priority 100 → 4.100)
|
||||||
#
|
#
|
||||||
# This ensures Admin > User > Project > Default hierarchy is always preserved,
|
# This ensures Admin > User > Workspace > Default hierarchy is always preserved,
|
||||||
# while allowing user-specified priorities to work within each tier.
|
# while allowing user-specified priorities to work within each tier.
|
||||||
#
|
#
|
||||||
# Settings-based and dynamic rules (all in user tier 3.x):
|
# Settings-based and dynamic rules (all in user tier 3.x):
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
#
|
#
|
||||||
# Priority bands (tiers):
|
# Priority bands (tiers):
|
||||||
# - Default policies (TOML): 1 + priority/1000 (e.g., priority 100 → 1.100)
|
# - Default policies (TOML): 1 + priority/1000 (e.g., priority 100 → 1.100)
|
||||||
# - Project policies (TOML): 2 + priority/1000 (e.g., priority 100 → 2.100)
|
# - Workspace policies (TOML): 2 + priority/1000 (e.g., priority 100 → 2.100)
|
||||||
# - User policies (TOML): 3 + priority/1000 (e.g., priority 100 → 3.100)
|
# - User policies (TOML): 3 + priority/1000 (e.g., priority 100 → 3.100)
|
||||||
# - Admin policies (TOML): 4 + priority/1000 (e.g., priority 100 → 4.100)
|
# - Admin policies (TOML): 4 + priority/1000 (e.g., priority 100 → 4.100)
|
||||||
#
|
#
|
||||||
# This ensures Admin > User > Project > Default hierarchy is always preserved,
|
# This ensures Admin > User > Workspace > Default hierarchy is always preserved,
|
||||||
# while allowing user-specified priorities to work within each tier.
|
# while allowing user-specified priorities to work within each tier.
|
||||||
#
|
#
|
||||||
# Settings-based and dynamic rules (all in user tier 3.x):
|
# Settings-based and dynamic rules (all in user tier 3.x):
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ modes = ["autoEdit"]
|
|||||||
expect(result2.rules).toHaveLength(1);
|
expect(result2.rules).toHaveLength(1);
|
||||||
expect(result2.rules[0].toolName).toBe('tier2-tool');
|
expect(result2.rules[0].toolName).toBe('tier2-tool');
|
||||||
expect(result2.rules[0].modes).toEqual(['autoEdit']);
|
expect(result2.rules[0].modes).toEqual(['autoEdit']);
|
||||||
expect(result2.rules[0].source).toBe('Project: tier2.toml');
|
expect(result2.rules[0].source).toBe('Workspace: tier2.toml');
|
||||||
|
|
||||||
const getPolicyTier3 = (_dir: string) => 3; // Tier 3
|
const getPolicyTier3 = (_dir: string) => 3; // Tier 3
|
||||||
const result3 = await loadPoliciesFromToml([tempDir], getPolicyTier3);
|
const result3 = await loadPoliciesFromToml([tempDir], getPolicyTier3);
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export type PolicyFileErrorType =
|
|||||||
export interface PolicyFileError {
|
export interface PolicyFileError {
|
||||||
filePath: string;
|
filePath: string;
|
||||||
fileName: string;
|
fileName: string;
|
||||||
tier: 'default' | 'user' | 'project' | 'admin';
|
tier: 'default' | 'user' | 'workspace' | 'admin';
|
||||||
ruleIndex?: number;
|
ruleIndex?: number;
|
||||||
errorType: PolicyFileErrorType;
|
errorType: PolicyFileErrorType;
|
||||||
message: string;
|
message: string;
|
||||||
@@ -172,9 +172,9 @@ export async function readPolicyFiles(
|
|||||||
/**
|
/**
|
||||||
* Converts a tier number to a human-readable tier name.
|
* Converts a tier number to a human-readable tier name.
|
||||||
*/
|
*/
|
||||||
function getTierName(tier: number): 'default' | 'user' | 'project' | 'admin' {
|
function getTierName(tier: number): 'default' | 'user' | 'workspace' | 'admin' {
|
||||||
if (tier === 1) return 'default';
|
if (tier === 1) return 'default';
|
||||||
if (tier === 2) return 'project';
|
if (tier === 2) return 'workspace';
|
||||||
if (tier === 3) return 'user';
|
if (tier === 3) return 'user';
|
||||||
if (tier === 4) return 'admin';
|
if (tier === 4) return 'admin';
|
||||||
return 'default';
|
return 'default';
|
||||||
|
|||||||
+21
-21
@@ -14,7 +14,7 @@ vi.mock('../utils/security.js', () => ({
|
|||||||
isDirectorySecure: vi.fn().mockResolvedValue({ secure: true }),
|
isDirectorySecure: vi.fn().mockResolvedValue({ secure: true }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('Project-Level Policies', () => {
|
describe('Workspace-Level Policies', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
const { Storage } = await import('../config/storage.js');
|
const { Storage } = await import('../config/storage.js');
|
||||||
@@ -34,8 +34,8 @@ describe('Project-Level Policies', () => {
|
|||||||
vi.doUnmock('node:fs/promises');
|
vi.doUnmock('node:fs/promises');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should load project policies with correct priority (Tier 2)', async () => {
|
it('should load workspace policies with correct priority (Tier 2)', async () => {
|
||||||
const projectPoliciesDir = '/mock/project/policies';
|
const workspacePoliciesDir = '/mock/workspace/policies';
|
||||||
const defaultPoliciesDir = '/mock/default/policies';
|
const defaultPoliciesDir = '/mock/default/policies';
|
||||||
|
|
||||||
// Mock FS
|
// Mock FS
|
||||||
@@ -69,10 +69,10 @@ describe('Project-Level Policies', () => {
|
|||||||
return [
|
return [
|
||||||
{ name: 'user.toml', isFile: () => true, isDirectory: () => false },
|
{ name: 'user.toml', isFile: () => true, isDirectory: () => false },
|
||||||
] as unknown as Awaited<ReturnType<typeof actualFs.readdir>>;
|
] as unknown as Awaited<ReturnType<typeof actualFs.readdir>>;
|
||||||
if (normalizedPath.endsWith('project/policies'))
|
if (normalizedPath.endsWith('workspace/policies'))
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'project.toml',
|
name: 'workspace.toml',
|
||||||
isFile: () => true,
|
isFile: () => true,
|
||||||
isDirectory: () => false,
|
isDirectory: () => false,
|
||||||
},
|
},
|
||||||
@@ -100,7 +100,7 @@ decision = "deny"
|
|||||||
priority = 10
|
priority = 10
|
||||||
`; // Tier 3 -> 3.010
|
`; // Tier 3 -> 3.010
|
||||||
}
|
}
|
||||||
if (path.includes('project.toml')) {
|
if (path.includes('workspace.toml')) {
|
||||||
return `[[rule]]
|
return `[[rule]]
|
||||||
toolName = "test_tool"
|
toolName = "test_tool"
|
||||||
decision = "allow"
|
decision = "allow"
|
||||||
@@ -132,12 +132,12 @@ priority = 10
|
|||||||
|
|
||||||
const { createPolicyEngineConfig } = await import('./config.js');
|
const { createPolicyEngineConfig } = await import('./config.js');
|
||||||
|
|
||||||
// Test 1: Project vs User (User should win)
|
// Test 1: Workspace vs User (User should win)
|
||||||
const config = await createPolicyEngineConfig(
|
const config = await createPolicyEngineConfig(
|
||||||
{},
|
{},
|
||||||
ApprovalMode.DEFAULT,
|
ApprovalMode.DEFAULT,
|
||||||
defaultPoliciesDir,
|
defaultPoliciesDir,
|
||||||
projectPoliciesDir,
|
workspacePoliciesDir,
|
||||||
);
|
);
|
||||||
|
|
||||||
const rules = config.rules?.filter((r) => r.toolName === 'test_tool');
|
const rules = config.rules?.filter((r) => r.toolName === 'test_tool');
|
||||||
@@ -145,22 +145,22 @@ priority = 10
|
|||||||
|
|
||||||
// Check for all 4 rules
|
// Check for all 4 rules
|
||||||
const defaultRule = rules?.find((r) => r.priority === 1.01);
|
const defaultRule = rules?.find((r) => r.priority === 1.01);
|
||||||
const projectRule = rules?.find((r) => r.priority === 2.01);
|
const workspaceRule = rules?.find((r) => r.priority === 2.01);
|
||||||
const userRule = rules?.find((r) => r.priority === 3.01);
|
const userRule = rules?.find((r) => r.priority === 3.01);
|
||||||
const adminRule = rules?.find((r) => r.priority === 4.01);
|
const adminRule = rules?.find((r) => r.priority === 4.01);
|
||||||
|
|
||||||
expect(defaultRule).toBeDefined();
|
expect(defaultRule).toBeDefined();
|
||||||
expect(userRule).toBeDefined();
|
expect(userRule).toBeDefined();
|
||||||
expect(projectRule).toBeDefined();
|
expect(workspaceRule).toBeDefined();
|
||||||
expect(adminRule).toBeDefined();
|
expect(adminRule).toBeDefined();
|
||||||
|
|
||||||
// Verify Hierarchy: Admin > User > Project > Default
|
// Verify Hierarchy: Admin > User > Workspace > Default
|
||||||
expect(adminRule!.priority).toBeGreaterThan(userRule!.priority!);
|
expect(adminRule!.priority).toBeGreaterThan(userRule!.priority!);
|
||||||
expect(userRule!.priority).toBeGreaterThan(projectRule!.priority!);
|
expect(userRule!.priority).toBeGreaterThan(workspaceRule!.priority!);
|
||||||
expect(projectRule!.priority).toBeGreaterThan(defaultRule!.priority!);
|
expect(workspaceRule!.priority).toBeGreaterThan(defaultRule!.priority!);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should ignore project policies if projectPoliciesDir is undefined', async () => {
|
it('should ignore workspace policies if workspacePoliciesDir is undefined', async () => {
|
||||||
const defaultPoliciesDir = '/mock/default/policies';
|
const defaultPoliciesDir = '/mock/default/policies';
|
||||||
|
|
||||||
// Mock FS (simplified)
|
// Mock FS (simplified)
|
||||||
@@ -217,7 +217,7 @@ priority=10`,
|
|||||||
{},
|
{},
|
||||||
ApprovalMode.DEFAULT,
|
ApprovalMode.DEFAULT,
|
||||||
defaultPoliciesDir,
|
defaultPoliciesDir,
|
||||||
undefined, // No project dir
|
undefined, // No workspace dir
|
||||||
);
|
);
|
||||||
|
|
||||||
// Should only have default tier rule (1.01)
|
// Should only have default tier rule (1.01)
|
||||||
@@ -226,8 +226,8 @@ priority=10`,
|
|||||||
expect(rules![0].priority).toBe(1.01);
|
expect(rules![0].priority).toBe(1.01);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should load project policies and correctly transform to Tier 2', async () => {
|
it('should load workspace policies and correctly transform to Tier 2', async () => {
|
||||||
const projectPoliciesDir = '/mock/project/policies';
|
const workspacePoliciesDir = '/mock/workspace/policies';
|
||||||
|
|
||||||
// Mock FS
|
// Mock FS
|
||||||
const actualFs =
|
const actualFs =
|
||||||
@@ -247,10 +247,10 @@ priority=10`,
|
|||||||
|
|
||||||
const mockReaddir = vi.fn(async (path: string) => {
|
const mockReaddir = vi.fn(async (path: string) => {
|
||||||
const normalizedPath = nodePath.normalize(path);
|
const normalizedPath = nodePath.normalize(path);
|
||||||
if (normalizedPath.endsWith('project/policies'))
|
if (normalizedPath.endsWith('workspace/policies'))
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'project.toml',
|
name: 'workspace.toml',
|
||||||
isFile: () => true,
|
isFile: () => true,
|
||||||
isDirectory: () => false,
|
isDirectory: () => false,
|
||||||
},
|
},
|
||||||
@@ -283,12 +283,12 @@ priority=500`,
|
|||||||
{},
|
{},
|
||||||
ApprovalMode.DEFAULT,
|
ApprovalMode.DEFAULT,
|
||||||
undefined,
|
undefined,
|
||||||
projectPoliciesDir,
|
workspacePoliciesDir,
|
||||||
);
|
);
|
||||||
|
|
||||||
const rule = config.rules?.find((r) => r.toolName === 'p_tool');
|
const rule = config.rules?.find((r) => r.toolName === 'p_tool');
|
||||||
expect(rule).toBeDefined();
|
expect(rule).toBeDefined();
|
||||||
// Project Tier (2) + 500/1000 = 2.5
|
// Workspace Tier (2) + 500/1000 = 2.5
|
||||||
expect(rule?.priority).toBe(2.5);
|
expect(rule?.priority).toBe(2.5);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user