refactor(permission-system): simplify policy and autoreview flow

This commit is contained in:
云服务部-叶林立
2026-09-03 02:01:06 +08:00
parent 180da95ab7
commit 6e7effa792
112 changed files with 911 additions and 10346 deletions
+8 -69
View File
@@ -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,
};