From 8feff1cc9b486bf5fd212251b6067f9cae5fd8cd Mon Sep 17 00:00:00 2001 From: Abhijit Balaji Date: Tue, 17 Feb 2026 11:08:10 -0800 Subject: [PATCH] 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. --- docs/core/policy-engine.md | 30 +++---- packages/cli/src/config/config.ts | 32 ++++---- packages/cli/src/config/policy.ts | 4 +- ...i.test.ts => workspace-policy-cli.test.ts} | 24 +++--- .../ui/components/PolicyUpdateDialog.test.tsx | 12 +-- packages/core/src/config/storage.ts | 2 +- packages/core/src/policy/config.ts | 32 ++++---- packages/core/src/policy/integrity.test.ts | 80 +++++++++---------- packages/core/src/policy/policies/plan.toml | 4 +- .../core/src/policy/policies/read-only.toml | 4 +- packages/core/src/policy/policies/write.toml | 4 +- packages/core/src/policy/policies/yolo.toml | 4 +- packages/core/src/policy/toml-loader.test.ts | 2 +- packages/core/src/policy/toml-loader.ts | 6 +- ...olicy.test.ts => workspace-policy.test.ts} | 42 +++++----- 15 files changed, 141 insertions(+), 141 deletions(-) rename packages/cli/src/config/{project-policy-cli.test.ts => workspace-policy-cli.test.ts} (89%) rename packages/core/src/policy/{project-policy.test.ts => workspace-policy.test.ts} (86%) diff --git a/docs/core/policy-engine.md b/docs/core/policy-engine.md index 352c34be99..2106b751c9 100644 --- a/docs/core/policy-engine.md +++ b/docs/core/policy-engine.md @@ -92,12 +92,12 @@ rule with the highest priority wins**. 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. -| Tier | Base | Description | -| :------ | :--- | :------------------------------------------------------------------------- | -| Default | 1 | Built-in policies that ship with the Gemini CLI. | -| Project | 2 | Policies defined in the current project's configuration directory. | -| User | 3 | Custom policies defined by the user. | -| Admin | 4 | Policies managed by an administrator (e.g., in an enterprise environment). | +| Tier | Base | Description | +| :-------- | :--- | :------------------------------------------------------------------------- | +| Default | 1 | Built-in policies that ship with the Gemini CLI. | +| Workspace | 2 | Policies defined in the current workspace's configuration directory. | +| User | 3 | Custom policies defined by the user. | +| 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 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: -- Admin policies always override User, Project, and Default policies. -- User policies override Project and Default policies. -- Project policies override Default policies. +- Admin policies always override User, Workspace, and Default policies. +- User policies override Workspace and Default policies. +- Workspace policies override Default policies. - You can still order rules within a single tier with fine-grained control. For example: - 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: 20` rule in an Admin policy file becomes `4.020`. @@ -159,11 +159,11 @@ User, and (if configured) Admin directories. ### Policy locations -| Tier | Type | Location | -| :---------- | :----- | :-------------------------------------- | -| **User** | Custom | `~/.gemini/policies/*.toml` | -| **Project** | Custom | `$PROJECT_ROOT/.gemini/policies/*.toml` | -| **Admin** | System | _See below (OS specific)_ | +| Tier | Type | Location | +| :------------ | :----- | :---------------------------------------- | +| **User** | Custom | `~/.gemini/policies/*.toml` | +| **Workspace** | Custom | `$WORKSPACE_ROOT/.gemini/policies/*.toml` | +| **Admin** | System | _See below (OS specific)_ | #### System-wide policies (Admin) diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 57f149a3d4..9c67fa87fa 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -294,7 +294,7 @@ export async function parseArguments( .option('accept-changed-policies', { type: 'boolean', description: - 'Automatically accept changed project policies (use with caution).', + 'Automatically accept changed workspace policies (use with caution).', }), ) // Register MCP subcommands @@ -702,52 +702,52 @@ export async function loadCliConfig( policyPaths: argv.policy, }; - let projectPoliciesDir: string | undefined; + let workspacePoliciesDir: string | undefined; let policyUpdateConfirmationRequest: | PolicyUpdateConfirmationRequest | undefined; if (trustedFolder) { - const potentialProjectPoliciesDir = new Storage( + const potentialWorkspacePoliciesDir = new Storage( cwd, - ).getProjectPoliciesDir(); + ).getWorkspacePoliciesDir(); const integrityManager = new PolicyIntegrityManager(); const integrityResult = await integrityManager.checkIntegrity( - 'project', + 'workspace', cwd, - potentialProjectPoliciesDir, + potentialWorkspacePoliciesDir, ); if (integrityResult.status === IntegrityStatus.MATCH) { - projectPoliciesDir = potentialProjectPoliciesDir; + workspacePoliciesDir = potentialWorkspacePoliciesDir; } else if ( integrityResult.status === IntegrityStatus.NEW && integrityResult.fileCount === 0 ) { - // No project policies found - projectPoliciesDir = undefined; + // No workspace policies found + workspacePoliciesDir = undefined; } else { // Policies changed or are new if (argv.acceptChangedPolicies) { 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( - 'project', + 'workspace', cwd, integrityResult.hash, ); - projectPoliciesDir = potentialProjectPoliciesDir; + workspacePoliciesDir = potentialWorkspacePoliciesDir; } else if (interactive) { policyUpdateConfirmationRequest = { - scope: 'project', + scope: 'workspace', identifier: cwd, - policyDir: potentialProjectPoliciesDir, + policyDir: potentialWorkspacePoliciesDir, newHash: integrityResult.hash, }; } else { 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( effectiveSettings, approvalMode, - projectPoliciesDir, + workspacePoliciesDir, ); policyEngineConfig.nonInteractive = !interactive; diff --git a/packages/cli/src/config/policy.ts b/packages/cli/src/config/policy.ts index 145a466a88..5d48271ede 100644 --- a/packages/cli/src/config/policy.ts +++ b/packages/cli/src/config/policy.ts @@ -18,7 +18,7 @@ import { type Settings } from './settings.js'; export async function createPolicyEngineConfig( settings: Settings, approvalMode: ApprovalMode, - projectPoliciesDir?: string, + workspacePoliciesDir?: string, ): Promise { // Explicitly construct PolicySettings from Settings to ensure type safety // and avoid accidental leakage of other settings properties. @@ -33,7 +33,7 @@ export async function createPolicyEngineConfig( policySettings, approvalMode, undefined, - projectPoliciesDir, + workspacePoliciesDir, ); } diff --git a/packages/cli/src/config/project-policy-cli.test.ts b/packages/cli/src/config/workspace-policy-cli.test.ts similarity index 89% rename from packages/cli/src/config/project-policy-cli.test.ts rename to packages/cli/src/config/workspace-policy-cli.test.ts index b7b8b94ba9..e6ad5bfc4c 100644 --- a/packages/cli/src/config/project-policy-cli.test.ts +++ b/packages/cli/src/config/workspace-policy-cli.test.ts @@ -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(); beforeEach(() => { @@ -63,13 +63,13 @@ describe('Project-Level Policy CLI Integration', () => { 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); - expect(storage.getProjectPoliciesDir).toBeDefined(); - expect(typeof storage.getProjectPoliciesDir).toBe('function'); + expect(storage.getWorkspacePoliciesDir).toBeDefined(); + 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({ isTrusted: true, 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({ isTrusted: false, 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({ isTrusted: true, 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({ isTrusted: true, source: 'file', @@ -149,7 +149,7 @@ describe('Project-Level Policy CLI Integration', () => { await loadCliConfig(settings, 'test-session', argv, { cwd: MOCK_CWD }); 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.anything(), @@ -176,7 +176,7 @@ describe('Project-Level Policy CLI Integration', () => { await loadCliConfig(settings, 'test-session', argv, { cwd: MOCK_CWD }); expect(mockAcceptIntegrity).toHaveBeenCalledWith( - 'project', + 'workspace', MOCK_CWD, 'new-hash', ); @@ -211,7 +211,7 @@ describe('Project-Level Policy CLI Integration', () => { }); expect(config.getPolicyUpdateConfirmationRequest()).toEqual({ - scope: 'project', + scope: 'workspace', identifier: MOCK_CWD, policyDir: expect.stringContaining(path.join('.gemini', 'policies')), newHash: 'new-hash', @@ -247,7 +247,7 @@ describe('Project-Level Policy CLI Integration', () => { }); expect(config.getPolicyUpdateConfirmationRequest()).toEqual({ - scope: 'project', + scope: 'workspace', identifier: MOCK_CWD, policyDir: expect.stringContaining(path.join('.gemini', 'policies')), newHash: 'new-hash', diff --git a/packages/cli/src/ui/components/PolicyUpdateDialog.test.tsx b/packages/cli/src/ui/components/PolicyUpdateDialog.test.tsx index ffc49e443b..5175564886 100644 --- a/packages/cli/src/ui/components/PolicyUpdateDialog.test.tsx +++ b/packages/cli/src/ui/components/PolicyUpdateDialog.test.tsx @@ -23,14 +23,14 @@ describe('PolicyUpdateDialog', () => { const { lastFrame } = renderWithProviders( , ); 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('Accept and Load'); expect(output).toContain('Ignore'); @@ -41,7 +41,7 @@ describe('PolicyUpdateDialog', () => { const { stdin } = renderWithProviders( , @@ -62,7 +62,7 @@ describe('PolicyUpdateDialog', () => { const { stdin } = renderWithProviders( , @@ -86,7 +86,7 @@ describe('PolicyUpdateDialog', () => { const { stdin } = renderWithProviders( , @@ -106,7 +106,7 @@ describe('PolicyUpdateDialog', () => { const { lastFrame } = renderWithProviders( , diff --git a/packages/core/src/config/storage.ts b/packages/core/src/config/storage.ts index 7071806377..5a3a58a008 100644 --- a/packages/core/src/config/storage.ts +++ b/packages/core/src/config/storage.ts @@ -140,7 +140,7 @@ export class Storage { return path.join(tempDir, identifier); } - getProjectPoliciesDir(): string { + getWorkspacePoliciesDir(): string { return path.join(this.getGeminiDir(), 'policies'); } diff --git a/packages/core/src/policy/config.ts b/packages/core/src/policy/config.ts index 6acb59f70b..a9414d65b6 100644 --- a/packages/core/src/policy/config.ts +++ b/packages/core/src/policy/config.ts @@ -38,7 +38,7 @@ export const DEFAULT_CORE_POLICIES_DIR = path.join(__dirname, 'policies'); // Policy tier constants for priority calculation 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 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 policyPaths Optional user-provided policy paths (from --policy flag). * 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( defaultPoliciesDir?: string, policyPaths?: string[], - projectPoliciesDir?: string, + workspacePoliciesDir?: string, ): string[] { const dirs = []; @@ -68,9 +68,9 @@ export function getPolicyDirectories( dirs.push(Storage.getUserPoliciesDir()); } - // Project Tier (third highest) - if (projectPoliciesDir) { - dirs.push(projectPoliciesDir); + // Workspace Tier (third highest) + if (workspacePoliciesDir) { + dirs.push(workspacePoliciesDir); } // 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. */ export function getPolicyTier( dir: string, defaultPoliciesDir?: string, - projectPoliciesDir?: string, + workspacePoliciesDir?: string, ): number { const USER_POLICIES_DIR = Storage.getUserPoliciesDir(); const ADMIN_POLICIES_DIR = Storage.getSystemPoliciesDir(); @@ -108,10 +108,10 @@ export function getPolicyTier( return USER_POLICY_TIER; } if ( - projectPoliciesDir && - normalizedDir === path.resolve(projectPoliciesDir) + workspacePoliciesDir && + normalizedDir === path.resolve(workspacePoliciesDir) ) { - return PROJECT_POLICY_TIER; + return WORKSPACE_POLICY_TIER; } if (normalizedDir === normalizedAdmin) { return ADMIN_POLICY_TIER; @@ -167,12 +167,12 @@ export async function createPolicyEngineConfig( settings: PolicySettings, approvalMode: ApprovalMode, defaultPoliciesDir?: string, - projectPoliciesDir?: string, + workspacePoliciesDir?: string, ): Promise { const policyDirs = getPolicyDirectories( defaultPoliciesDir, settings.policyPaths, - projectPoliciesDir, + workspacePoliciesDir, ); const securePolicyDirs = await filterSecurePolicyDirectories(policyDirs); @@ -186,7 +186,7 @@ export async function createPolicyEngineConfig( checkers: tomlCheckers, errors, } = 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, // treat it as USER tier. @@ -222,11 +222,11 @@ export async function createPolicyEngineConfig( // // Priority bands (tiers): // - 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) // - 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. // // Settings-based and dynamic rules (all in user tier 3.x): diff --git a/packages/core/src/policy/integrity.test.ts b/packages/core/src/policy/integrity.test.ts index c345914fed..a289c513d0 100644 --- a/packages/core/src/policy/integrity.test.ts +++ b/packages/core/src/policy/integrity.test.ts @@ -61,11 +61,11 @@ describe('PolicyIntegrityManager', () => { it('should return NEW if no stored hash', async () => { mockFs.readFile.mockRejectedValue({ code: 'ENOENT' }); // No stored file readPolicyFilesMock.mockResolvedValue([ - { path: '/project/policies/a.toml', content: 'contentA' }, + { path: '/workspace/policies/a.toml', content: 'contentA' }, ]); const result = await integrityManager.checkIntegrity( - 'project', + 'workspace', 'id', '/dir', ); @@ -77,13 +77,13 @@ describe('PolicyIntegrityManager', () => { it('should return MATCH if stored hash matches', async () => { 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. // But we can run checkIntegrity once (NEW) to get the hash, then mock FS with that hash. mockFs.readFile.mockRejectedValue({ code: 'ENOENT' }); const resultNew = await integrityManager.checkIntegrity( - 'project', + 'workspace', 'id', '/dir', ); @@ -91,12 +91,12 @@ describe('PolicyIntegrityManager', () => { mockFs.readFile.mockResolvedValue( JSON.stringify({ - 'project:id': currentHash, + 'workspace:id': currentHash, }), ); const result = await integrityManager.checkIntegrity( - 'project', + 'workspace', 'id', '/dir', ); @@ -108,10 +108,10 @@ describe('PolicyIntegrityManager', () => { it('should return MISMATCH if stored hash differs', async () => { mockFs.readFile.mockRejectedValue({ code: 'ENOENT' }); readPolicyFilesMock.mockResolvedValue([ - { path: '/project/policies/a.toml', content: 'contentA' }, + { path: '/workspace/policies/a.toml', content: 'contentA' }, ]); const resultNew = await integrityManager.checkIntegrity( - 'project', + 'workspace', 'id', '/dir', ); @@ -119,12 +119,12 @@ describe('PolicyIntegrityManager', () => { mockFs.readFile.mockResolvedValue( JSON.stringify({ - 'project:id': 'different_hash', + 'workspace:id': 'different_hash', }), ); const result = await integrityManager.checkIntegrity( - 'project', + 'workspace', 'id', '/dir', ); @@ -137,21 +137,21 @@ describe('PolicyIntegrityManager', () => { mockFs.readFile.mockRejectedValue({ code: 'ENOENT' }); readPolicyFilesMock.mockResolvedValue([ - { path: '/project/policies/a.toml', content: 'contentA' }, + { path: '/workspace/policies/a.toml', content: 'contentA' }, ]); const result1 = await integrityManager.checkIntegrity( - 'project', + 'workspace', 'id', - '/project/policies', + '/workspace/policies', ); readPolicyFilesMock.mockResolvedValue([ - { path: '/project/policies/b.toml', content: 'contentA' }, + { path: '/workspace/policies/b.toml', content: 'contentA' }, ]); const result2 = await integrityManager.checkIntegrity( - 'project', + 'workspace', 'id', - '/project/policies', + '/workspace/policies', ); expect(result1.hash).not.toBe(result2.hash); @@ -161,21 +161,21 @@ describe('PolicyIntegrityManager', () => { mockFs.readFile.mockRejectedValue({ code: 'ENOENT' }); readPolicyFilesMock.mockResolvedValue([ - { path: '/project/policies/a.toml', content: 'contentA' }, + { path: '/workspace/policies/a.toml', content: 'contentA' }, ]); const result1 = await integrityManager.checkIntegrity( - 'project', + 'workspace', 'id', - '/project/policies', + '/workspace/policies', ); readPolicyFilesMock.mockResolvedValue([ - { path: '/project/policies/a.toml', content: 'contentB' }, + { path: '/workspace/policies/a.toml', content: 'contentB' }, ]); const result2 = await integrityManager.checkIntegrity( - 'project', + 'workspace', 'id', - '/project/policies', + '/workspace/policies', ); expect(result1.hash).not.toBe(result2.hash); @@ -185,23 +185,23 @@ describe('PolicyIntegrityManager', () => { mockFs.readFile.mockRejectedValue({ code: 'ENOENT' }); readPolicyFilesMock.mockResolvedValue([ - { path: '/project/policies/a.toml', content: 'contentA' }, - { path: '/project/policies/b.toml', content: 'contentB' }, + { path: '/workspace/policies/a.toml', content: 'contentA' }, + { path: '/workspace/policies/b.toml', content: 'contentB' }, ]); const result1 = await integrityManager.checkIntegrity( - 'project', + 'workspace', 'id', - '/project/policies', + '/workspace/policies', ); readPolicyFilesMock.mockResolvedValue([ - { path: '/project/policies/b.toml', content: 'contentB' }, - { path: '/project/policies/a.toml', content: 'contentA' }, + { path: '/workspace/policies/b.toml', content: 'contentB' }, + { path: '/workspace/policies/a.toml', content: 'contentA' }, ]); const result2 = await integrityManager.checkIntegrity( - 'project', + 'workspace', 'id', - '/project/policies', + '/workspace/policies', ); expect(result1.hash).toBe(result2.hash); @@ -215,7 +215,7 @@ describe('PolicyIntegrityManager', () => { { path: '/dirA/p.toml', content: 'contentA' }, ]); const { hash: hashA } = await integrityManager.checkIntegrity( - 'project', + 'workspace', 'idA', '/dirA', ); @@ -224,7 +224,7 @@ describe('PolicyIntegrityManager', () => { { path: '/dirB/p.toml', content: 'contentB' }, ]); const { hash: hashB } = await integrityManager.checkIntegrity( - 'project', + 'workspace', 'idB', '/dirB', ); @@ -232,8 +232,8 @@ describe('PolicyIntegrityManager', () => { // Now mock storage with both mockFs.readFile.mockResolvedValue( JSON.stringify({ - 'project:idA': hashA, - 'project:idB': 'oldHashB', // Different from hashB + 'workspace:idA': hashA, + 'workspace:idB': 'oldHashB', // Different from hashB }), ); @@ -242,7 +242,7 @@ describe('PolicyIntegrityManager', () => { { path: '/dirA/p.toml', content: 'contentA' }, ]); const resultA = await integrityManager.checkIntegrity( - 'project', + 'workspace', 'idA', '/dirA', ); @@ -254,7 +254,7 @@ describe('PolicyIntegrityManager', () => { { path: '/dirB/p.toml', content: 'contentB' }, ]); const resultB = await integrityManager.checkIntegrity( - 'project', + 'workspace', 'idB', '/dirB', ); @@ -269,11 +269,11 @@ describe('PolicyIntegrityManager', () => { mockFs.mkdir.mockResolvedValue(undefined); mockFs.writeFile.mockResolvedValue(undefined); - await integrityManager.acceptIntegrity('project', 'id', 'hash123'); + await integrityManager.acceptIntegrity('workspace', 'id', 'hash123'); expect(mockFs.writeFile).toHaveBeenCalledWith( '/mock/storage/policy_integrity.json', - JSON.stringify({ 'project:id': 'hash123' }, null, 2), + JSON.stringify({ 'workspace:id': 'hash123' }, null, 2), 'utf-8', ); }); @@ -287,14 +287,14 @@ describe('PolicyIntegrityManager', () => { mockFs.mkdir.mockResolvedValue(undefined); mockFs.writeFile.mockResolvedValue(undefined); - await integrityManager.acceptIntegrity('project', 'id', 'hash123'); + await integrityManager.acceptIntegrity('workspace', 'id', 'hash123'); expect(mockFs.writeFile).toHaveBeenCalledWith( '/mock/storage/policy_integrity.json', JSON.stringify( { 'other:id': 'otherhash', - 'project:id': 'hash123', + 'workspace:id': 'hash123', }, null, 2, diff --git a/packages/core/src/policy/policies/plan.toml b/packages/core/src/policy/policies/plan.toml index 2bd18554d0..e7129208c8 100644 --- a/packages/core/src/policy/policies/plan.toml +++ b/packages/core/src/policy/policies/plan.toml @@ -5,11 +5,11 @@ # # Priority bands (tiers): # - 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) # - 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. # # Settings-based and dynamic rules (all in user tier 3.x): diff --git a/packages/core/src/policy/policies/read-only.toml b/packages/core/src/policy/policies/read-only.toml index 41f6b2205b..1688d5108c 100644 --- a/packages/core/src/policy/policies/read-only.toml +++ b/packages/core/src/policy/policies/read-only.toml @@ -5,11 +5,11 @@ # # Priority bands (tiers): # - 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) # - 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. # # Settings-based and dynamic rules (all in user tier 3.x): diff --git a/packages/core/src/policy/policies/write.toml b/packages/core/src/policy/policies/write.toml index 8f1e3d33e1..47cd9c98ae 100644 --- a/packages/core/src/policy/policies/write.toml +++ b/packages/core/src/policy/policies/write.toml @@ -5,11 +5,11 @@ # # Priority bands (tiers): # - 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) # - 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. # # Settings-based and dynamic rules (all in user tier 3.x): diff --git a/packages/core/src/policy/policies/yolo.toml b/packages/core/src/policy/policies/yolo.toml index 174099d7ee..332334db7c 100644 --- a/packages/core/src/policy/policies/yolo.toml +++ b/packages/core/src/policy/policies/yolo.toml @@ -5,11 +5,11 @@ # # Priority bands (tiers): # - 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) # - 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. # # Settings-based and dynamic rules (all in user tier 3.x): diff --git a/packages/core/src/policy/toml-loader.test.ts b/packages/core/src/policy/toml-loader.test.ts index 115c758063..af3ecc1bda 100644 --- a/packages/core/src/policy/toml-loader.test.ts +++ b/packages/core/src/policy/toml-loader.test.ts @@ -234,7 +234,7 @@ modes = ["autoEdit"] expect(result2.rules).toHaveLength(1); expect(result2.rules[0].toolName).toBe('tier2-tool'); 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 result3 = await loadPoliciesFromToml([tempDir], getPolicyTier3); diff --git a/packages/core/src/policy/toml-loader.ts b/packages/core/src/policy/toml-loader.ts index 53b0c6b3fd..b1fa63cf83 100644 --- a/packages/core/src/policy/toml-loader.ts +++ b/packages/core/src/policy/toml-loader.ts @@ -105,7 +105,7 @@ export type PolicyFileErrorType = export interface PolicyFileError { filePath: string; fileName: string; - tier: 'default' | 'user' | 'project' | 'admin'; + tier: 'default' | 'user' | 'workspace' | 'admin'; ruleIndex?: number; errorType: PolicyFileErrorType; message: string; @@ -172,9 +172,9 @@ export async function readPolicyFiles( /** * 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 === 2) return 'project'; + if (tier === 2) return 'workspace'; if (tier === 3) return 'user'; if (tier === 4) return 'admin'; return 'default'; diff --git a/packages/core/src/policy/project-policy.test.ts b/packages/core/src/policy/workspace-policy.test.ts similarity index 86% rename from packages/core/src/policy/project-policy.test.ts rename to packages/core/src/policy/workspace-policy.test.ts index b3592f71e5..fea2ee39db 100644 --- a/packages/core/src/policy/project-policy.test.ts +++ b/packages/core/src/policy/workspace-policy.test.ts @@ -14,7 +14,7 @@ vi.mock('../utils/security.js', () => ({ isDirectorySecure: vi.fn().mockResolvedValue({ secure: true }), })); -describe('Project-Level Policies', () => { +describe('Workspace-Level Policies', () => { beforeEach(async () => { vi.resetModules(); const { Storage } = await import('../config/storage.js'); @@ -34,8 +34,8 @@ describe('Project-Level Policies', () => { vi.doUnmock('node:fs/promises'); }); - it('should load project policies with correct priority (Tier 2)', async () => { - const projectPoliciesDir = '/mock/project/policies'; + it('should load workspace policies with correct priority (Tier 2)', async () => { + const workspacePoliciesDir = '/mock/workspace/policies'; const defaultPoliciesDir = '/mock/default/policies'; // Mock FS @@ -69,10 +69,10 @@ describe('Project-Level Policies', () => { return [ { name: 'user.toml', isFile: () => true, isDirectory: () => false }, ] as unknown as Awaited>; - if (normalizedPath.endsWith('project/policies')) + if (normalizedPath.endsWith('workspace/policies')) return [ { - name: 'project.toml', + name: 'workspace.toml', isFile: () => true, isDirectory: () => false, }, @@ -100,7 +100,7 @@ decision = "deny" priority = 10 `; // Tier 3 -> 3.010 } - if (path.includes('project.toml')) { + if (path.includes('workspace.toml')) { return `[[rule]] toolName = "test_tool" decision = "allow" @@ -132,12 +132,12 @@ priority = 10 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( {}, ApprovalMode.DEFAULT, defaultPoliciesDir, - projectPoliciesDir, + workspacePoliciesDir, ); const rules = config.rules?.filter((r) => r.toolName === 'test_tool'); @@ -145,22 +145,22 @@ priority = 10 // Check for all 4 rules 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 adminRule = rules?.find((r) => r.priority === 4.01); expect(defaultRule).toBeDefined(); expect(userRule).toBeDefined(); - expect(projectRule).toBeDefined(); + expect(workspaceRule).toBeDefined(); expect(adminRule).toBeDefined(); - // Verify Hierarchy: Admin > User > Project > Default + // Verify Hierarchy: Admin > User > Workspace > Default expect(adminRule!.priority).toBeGreaterThan(userRule!.priority!); - expect(userRule!.priority).toBeGreaterThan(projectRule!.priority!); - expect(projectRule!.priority).toBeGreaterThan(defaultRule!.priority!); + expect(userRule!.priority).toBeGreaterThan(workspaceRule!.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'; // Mock FS (simplified) @@ -217,7 +217,7 @@ priority=10`, {}, ApprovalMode.DEFAULT, defaultPoliciesDir, - undefined, // No project dir + undefined, // No workspace dir ); // Should only have default tier rule (1.01) @@ -226,8 +226,8 @@ priority=10`, expect(rules![0].priority).toBe(1.01); }); - it('should load project policies and correctly transform to Tier 2', async () => { - const projectPoliciesDir = '/mock/project/policies'; + it('should load workspace policies and correctly transform to Tier 2', async () => { + const workspacePoliciesDir = '/mock/workspace/policies'; // Mock FS const actualFs = @@ -247,10 +247,10 @@ priority=10`, const mockReaddir = vi.fn(async (path: string) => { const normalizedPath = nodePath.normalize(path); - if (normalizedPath.endsWith('project/policies')) + if (normalizedPath.endsWith('workspace/policies')) return [ { - name: 'project.toml', + name: 'workspace.toml', isFile: () => true, isDirectory: () => false, }, @@ -283,12 +283,12 @@ priority=500`, {}, ApprovalMode.DEFAULT, undefined, - projectPoliciesDir, + workspacePoliciesDir, ); const rule = config.rules?.find((r) => r.toolName === 'p_tool'); expect(rule).toBeDefined(); - // Project Tier (2) + 500/1000 = 2.5 + // Workspace Tier (2) + 500/1000 = 2.5 expect(rule?.priority).toBe(2.5); }); });