mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 18:55:22 +00:00
refactor(permission-system): simplify policy and autoreview flow
This commit is contained in:
@@ -5,8 +5,6 @@ import {
|
||||
getGlobalConfigPath,
|
||||
getLegacyExtensionConfigPath,
|
||||
getLegacyGlobalPolicyPath,
|
||||
getLegacyProjectPolicyPath,
|
||||
getProjectConfigPath,
|
||||
} from "./config-paths";
|
||||
import {
|
||||
type ShellToolsConfig,
|
||||
@@ -194,8 +192,6 @@ function formatConfigIssues(error: ZodError): string[] {
|
||||
* - `permission` is deep-shallow merged (surface-level object maps are shallow-merged).
|
||||
* - Scalar fields (debugLog, permissionReviewLog, yoloMode) are replaced when
|
||||
* present in the override.
|
||||
* - Array fields (piInfrastructureReadPaths) replace the base when present in
|
||||
* the override (override-wins, same as scalars).
|
||||
*/
|
||||
// Scalar knobs merged by override-replaces-base; keep in sync with
|
||||
// PermissionSystemExtensionConfig booleans (debugLog, permissionReviewLog,
|
||||
@@ -235,11 +231,9 @@ export function mergeUnifiedConfigs(
|
||||
}
|
||||
|
||||
// Array fields: override replaces base when defined
|
||||
for (const key of ["piInfrastructureReadPaths", "authorizerChain"] as const) {
|
||||
const value = override[key] ?? base[key];
|
||||
if (value !== undefined) {
|
||||
merged[key] = value;
|
||||
}
|
||||
const authorizerChain = override.authorizerChain ?? base.authorizerChain;
|
||||
if (authorizerChain !== undefined) {
|
||||
merged.authorizerChain = authorizerChain;
|
||||
}
|
||||
|
||||
// shellTools: merge by tool name, then by descriptor field. A project can
|
||||
@@ -286,51 +280,25 @@ export function mergeUnifiedConfigs(
|
||||
|
||||
export interface MergedConfigResult {
|
||||
global: UnifiedPermissionConfig;
|
||||
project: UnifiedPermissionConfig;
|
||||
merged: UnifiedPermissionConfig;
|
||||
issues: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load global and project configs from the new layout, detect legacy files,
|
||||
* merge everything, and collect issues.
|
||||
*
|
||||
* Merge order:
|
||||
* 1. Legacy global policy (if present) — lowest precedence
|
||||
* 2. Legacy extension runtime config (if present and path differs from new global)
|
||||
* 3. New global config
|
||||
* 4. Legacy project policy (if present)
|
||||
* 5. New project config — highest precedence
|
||||
*
|
||||
* Legacy files are detected and warned about. Their content is parsed with the
|
||||
* flat-format parser — legacy-format keys (defaultPolicy, tools, bash, etc.)
|
||||
* are not translated and contribute no permission rules.
|
||||
*
|
||||
* When `options.includeProjectScope` is `false`, the project-scope steps (4 and
|
||||
* 5) are skipped entirely — neither the legacy project policy nor the new
|
||||
* project config is read or merged. This gates project-local config on project
|
||||
* trust: an untrusted repository cannot loosen the operator's global policy
|
||||
* (#644). It defaults to `true`, preserving the trusted / caller-agnostic path.
|
||||
* Load the user-global config, detect legacy global files, and collect issues.
|
||||
* Project-level `policy.jsonc` files are intentionally not loaded.
|
||||
*/
|
||||
export function loadAndMergeConfigs(
|
||||
agentDir: string,
|
||||
cwd: string,
|
||||
_cwd: string,
|
||||
extensionRoot: string,
|
||||
options: { includeProjectScope?: boolean } = {},
|
||||
): MergedConfigResult {
|
||||
const includeProjectScope = options.includeProjectScope !== false;
|
||||
const allIssues: string[] = [];
|
||||
|
||||
const newGlobalPath = getGlobalConfigPath(agentDir);
|
||||
const newProjectPath = getProjectConfigPath(cwd);
|
||||
const legacyGlobalPolicyPath = getLegacyGlobalPolicyPath(agentDir);
|
||||
const legacyProjectPolicyPath = getLegacyProjectPolicyPath(cwd);
|
||||
const legacyExtConfigPath = getLegacyExtensionConfigPath(extensionRoot);
|
||||
|
||||
// Start with empty
|
||||
let merged: UnifiedPermissionConfig = {};
|
||||
|
||||
// 1. Legacy global policy
|
||||
if (existsSync(legacyGlobalPolicyPath)) {
|
||||
const legacy = loadUnifiedConfig(legacyGlobalPolicyPath);
|
||||
allIssues.push(
|
||||
@@ -338,55 +306,27 @@ export function loadAndMergeConfigs(
|
||||
`Move it to '${newGlobalPath}':\n` +
|
||||
` mv '${legacyGlobalPolicyPath}' '${newGlobalPath}'`,
|
||||
);
|
||||
// Legacy files are migrated away; the move-it guidance above is the
|
||||
// actionable signal, so strict-validation issues for them are suppressed.
|
||||
merged = mergeUnifiedConfigs(merged, legacy.config);
|
||||
}
|
||||
|
||||
// 2. Legacy extension runtime config (only if different from new global path)
|
||||
const normalizedLegacyExt = normalize(legacyExtConfigPath);
|
||||
const normalizedNewGlobal = normalize(newGlobalPath);
|
||||
if (
|
||||
normalizedLegacyExt !== normalizedNewGlobal &&
|
||||
normalize(legacyExtConfigPath) !== normalize(newGlobalPath) &&
|
||||
existsSync(legacyExtConfigPath)
|
||||
) {
|
||||
const legacy = loadUnifiedConfig(legacyExtConfigPath);
|
||||
allIssues.push(
|
||||
`Legacy extension config found at '${legacyExtConfigPath}'. ` +
|
||||
`Move runtime settings to '${newGlobalPath}':\n` +
|
||||
`Move it to '${newGlobalPath}':\n` +
|
||||
` mv '${legacyExtConfigPath}' '${newGlobalPath}'`,
|
||||
);
|
||||
// See above: legacy-file validation issues are suppressed.
|
||||
merged = mergeUnifiedConfigs(merged, legacy.config);
|
||||
}
|
||||
|
||||
// 3. New global config
|
||||
const globalResult = loadUnifiedConfig(newGlobalPath);
|
||||
allIssues.push(...globalResult.issues);
|
||||
const globalConfig = globalResult.config;
|
||||
merged = mergeUnifiedConfigs(merged, globalConfig);
|
||||
|
||||
// 4. Legacy project policy — skipped when the project scope is withheld.
|
||||
if (includeProjectScope && existsSync(legacyProjectPolicyPath)) {
|
||||
const legacy = loadUnifiedConfig(legacyProjectPolicyPath);
|
||||
allIssues.push(
|
||||
`Legacy project policy found at '${legacyProjectPolicyPath}'. ` +
|
||||
`Move it to '${newProjectPath}':\n` +
|
||||
` mv '${legacyProjectPolicyPath}' '${newProjectPath}'`,
|
||||
);
|
||||
// See above: legacy-file validation issues are suppressed.
|
||||
merged = mergeUnifiedConfigs(merged, legacy.config);
|
||||
}
|
||||
|
||||
// 5. New project config — skipped when the project scope is withheld, so an
|
||||
// untrusted project contributes nothing and `project` reports empty.
|
||||
const projectResult = includeProjectScope
|
||||
? loadUnifiedConfig(newProjectPath)
|
||||
: { config: {}, issues: [] };
|
||||
allIssues.push(...projectResult.issues);
|
||||
const projectConfig = projectResult.config;
|
||||
merged = mergeUnifiedConfigs(merged, projectConfig);
|
||||
|
||||
const bashFallbackIssue = detectPermissiveBashFallback(merged.permission);
|
||||
if (bashFallbackIssue) allIssues.push(bashFallbackIssue);
|
||||
|
||||
@@ -395,7 +335,6 @@ export function loadAndMergeConfigs(
|
||||
|
||||
return {
|
||||
global: globalConfig,
|
||||
project: projectConfig,
|
||||
merged,
|
||||
issues: allIssues,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user