rulesync 9.2.0 → 9.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.cjs +5 -62
- package/dist/cli/index.js +3 -60
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-BkxwFCDM.js → import-BxqZVTKb.js} +1799 -509
- package/dist/import-BxqZVTKb.js.map +1 -0
- package/dist/{import-H0MKtu9x.cjs → import-DVGMvuhx.cjs} +1813 -619
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/import-BkxwFCDM.js.map +0 -1
|
@@ -3003,8 +3003,8 @@ const DEVIN_SKILLS_DIR_PATH = join(DEVIN_DIR, "skills");
|
|
|
3003
3003
|
const DEVIN_AGENTS_DIR_PATH = join(DEVIN_DIR, "agents");
|
|
3004
3004
|
const DEVIN_GLOBAL_CONFIG_DIR_PATH = join(".config", "devin");
|
|
3005
3005
|
const DEVIN_GLOBAL_AGENTS_DIR_PATH = join(DEVIN_GLOBAL_CONFIG_DIR_PATH, "agents");
|
|
3006
|
+
const DEVIN_GLOBAL_SKILLS_DIR_PATH = join(DEVIN_GLOBAL_CONFIG_DIR_PATH, "skills");
|
|
3006
3007
|
const CODEIUM_WINDSURF_GLOBAL_WORKFLOWS_DIR_PATH = join(CODEIUM_WINDSURF_DIR, "global_workflows");
|
|
3007
|
-
const CODEIUM_WINDSURF_SKILLS_DIR_PATH = join(CODEIUM_WINDSURF_DIR, "skills");
|
|
3008
3008
|
const DEVIN_CONFIG_FILE_NAME = "config.json";
|
|
3009
3009
|
const DEVIN_HOOKS_V1_FILE_NAME = "hooks.v1.json";
|
|
3010
3010
|
const DEVIN_GLOBAL_AGENTS_FILE_NAME = "AGENTS.md";
|
|
@@ -3892,6 +3892,297 @@ const OPENCODE_JSON_FILE_NAME = "opencode.json";
|
|
|
3892
3892
|
const OPENCODE_RULE_FILE_NAME = "AGENTS.md";
|
|
3893
3893
|
const OPENCODE_HOOKS_FILE_NAME = "rulesync-hooks.js";
|
|
3894
3894
|
//#endregion
|
|
3895
|
+
//#region src/constants/takt-paths.ts
|
|
3896
|
+
const TAKT_DIR = ".takt";
|
|
3897
|
+
const TAKT_FACETS_SUBDIR = "facets";
|
|
3898
|
+
const TAKT_FACETS_DIR_PATH = join(TAKT_DIR, TAKT_FACETS_SUBDIR);
|
|
3899
|
+
const TAKT_RULES_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "policies");
|
|
3900
|
+
const TAKT_COMMANDS_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "instructions");
|
|
3901
|
+
const TAKT_SKILLS_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "knowledge");
|
|
3902
|
+
const TAKT_SUBAGENTS_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "personas");
|
|
3903
|
+
const TAKT_OUTPUT_CONTRACTS_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "output-contracts");
|
|
3904
|
+
const TAKT_RULE_OVERVIEW_FILE_NAME = "overview.md";
|
|
3905
|
+
/**
|
|
3906
|
+
* Takt's shared config file. Lives at `.takt/config.yaml` (project) and
|
|
3907
|
+
* `~/.takt/config.yaml` (global); it holds the active provider, provider
|
|
3908
|
+
* profiles (including permission modes), and other Takt settings.
|
|
3909
|
+
* @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
|
|
3910
|
+
*/
|
|
3911
|
+
const TAKT_CONFIG_FILE_NAME = "config.yaml";
|
|
3912
|
+
/**
|
|
3913
|
+
* Top-level key in Takt's `config.yaml` holding the workflow MCP transport
|
|
3914
|
+
* allowlist (`stdio` / `sse` / `http` booleans). Takt is default-deny: a
|
|
3915
|
+
* transport must be explicitly enabled here before any workflow-defined MCP
|
|
3916
|
+
* server using it is permitted to run.
|
|
3917
|
+
* @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
|
|
3918
|
+
*/
|
|
3919
|
+
const TAKT_WORKFLOW_MCP_SERVERS_KEY = "workflow_mcp_servers";
|
|
3920
|
+
//#endregion
|
|
3921
|
+
//#region src/utils/prototype-pollution.ts
|
|
3922
|
+
/**
|
|
3923
|
+
* Keys that, if walked into when constructing or merging objects from
|
|
3924
|
+
* untrusted input, can mutate `Object.prototype` (or otherwise the prototype
|
|
3925
|
+
* chain) and propagate state to every other object in the runtime. Any code
|
|
3926
|
+
* that copies arbitrary user-supplied keys into a fresh object — frontmatter
|
|
3927
|
+
* parsing, MCP config conversion, settings round-trip — should skip these.
|
|
3928
|
+
*/
|
|
3929
|
+
const PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
|
|
3930
|
+
"__proto__",
|
|
3931
|
+
"constructor",
|
|
3932
|
+
"prototype"
|
|
3933
|
+
]);
|
|
3934
|
+
function isPrototypePollutionKey(key) {
|
|
3935
|
+
return PROTOTYPE_POLLUTION_KEYS.has(key);
|
|
3936
|
+
}
|
|
3937
|
+
/**
|
|
3938
|
+
* Returns a shallow copy of a record's own entries with every
|
|
3939
|
+
* prototype-pollution key (`__proto__`, `constructor`, `prototype`) dropped.
|
|
3940
|
+
*
|
|
3941
|
+
* Use when copying a nested, user-supplied string map — an MCP server's `env`
|
|
3942
|
+
* or `headers` table — into freshly generated config. Carrying such a map by
|
|
3943
|
+
* reference, or re-assigning its keys via bracket notation, would let a literal
|
|
3944
|
+
* `__proto__` key ride along (and re-assigning it would mutate the target's
|
|
3945
|
+
* prototype). Walking the entries through this helper severs that path while
|
|
3946
|
+
* preserving every legitimate key.
|
|
3947
|
+
*/
|
|
3948
|
+
function omitPrototypePollutionKeys(record) {
|
|
3949
|
+
const sanitized = {};
|
|
3950
|
+
for (const [key, value] of Object.entries(record)) {
|
|
3951
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
3952
|
+
sanitized[key] = value;
|
|
3953
|
+
}
|
|
3954
|
+
return sanitized;
|
|
3955
|
+
}
|
|
3956
|
+
//#endregion
|
|
3957
|
+
//#region src/features/shared/shared-config-gateway.ts
|
|
3958
|
+
function sanitizeSharedConfigValue(value) {
|
|
3959
|
+
if (Array.isArray(value)) return value.map(sanitizeSharedConfigValue);
|
|
3960
|
+
if (!isPlainObject(value)) return value;
|
|
3961
|
+
const result = {};
|
|
3962
|
+
for (const [key, nested] of Object.entries(omitPrototypePollutionKeys(value))) result[key] = sanitizeSharedConfigValue(nested);
|
|
3963
|
+
return result;
|
|
3964
|
+
}
|
|
3965
|
+
/**
|
|
3966
|
+
* Parse a shared config file into a plain document: an empty/whitespace file
|
|
3967
|
+
* is `{}`, prototype-pollution keys are dropped recursively, and a non-mapping
|
|
3968
|
+
* root follows `invalidRootPolicy`. Syntax errors are wrapped with the file
|
|
3969
|
+
* path when one is given.
|
|
3970
|
+
*/
|
|
3971
|
+
function parseSharedConfig({ format, fileContent, filePath, invalidRootPolicy = "coerce-empty" }) {
|
|
3972
|
+
if (fileContent.trim() === "") return {};
|
|
3973
|
+
const at = filePath === void 0 ? "" : ` at ${filePath}`;
|
|
3974
|
+
let parsed;
|
|
3975
|
+
try {
|
|
3976
|
+
if (format === "yaml") parsed = load(fileContent);
|
|
3977
|
+
else if (format === "json") parsed = JSON.parse(fileContent);
|
|
3978
|
+
else parsed = parse(fileContent);
|
|
3979
|
+
} catch (error) {
|
|
3980
|
+
throw new Error(`Failed to parse shared config${at}: ${formatError(error)}`, { cause: error });
|
|
3981
|
+
}
|
|
3982
|
+
if (parsed === void 0 || parsed === null) return {};
|
|
3983
|
+
if (!isPlainObject(parsed)) {
|
|
3984
|
+
if (invalidRootPolicy === "error") throw new Error(`Failed to parse shared config${at}: expected a mapping at the root`);
|
|
3985
|
+
return {};
|
|
3986
|
+
}
|
|
3987
|
+
return sanitizeSharedConfigValue(parsed);
|
|
3988
|
+
}
|
|
3989
|
+
/**
|
|
3990
|
+
* Serialize a shared config document. YAML output always ends with exactly one
|
|
3991
|
+
* newline; JSON output matches the 2-space `JSON.stringify` shape the JSON
|
|
3992
|
+
* writers have always emitted (no trailing newline).
|
|
3993
|
+
*/
|
|
3994
|
+
function stringifySharedConfig({ format, document }) {
|
|
3995
|
+
if (format === "yaml") return dump(document, {
|
|
3996
|
+
noRefs: true,
|
|
3997
|
+
sortKeys: false
|
|
3998
|
+
}).trimEnd() + "\n";
|
|
3999
|
+
return JSON.stringify(document, null, 2);
|
|
4000
|
+
}
|
|
4001
|
+
/**
|
|
4002
|
+
* Shallow merge: every top-level key in `patch` replaces the base key
|
|
4003
|
+
* wholesale; all other base keys are preserved. The policy for a feature that
|
|
4004
|
+
* owns a fixed set of top-level keys.
|
|
4005
|
+
*/
|
|
4006
|
+
function mergeSharedConfigShallow({ base, patch }) {
|
|
4007
|
+
return {
|
|
4008
|
+
...base,
|
|
4009
|
+
...sanitizeSharedConfigValue(patch)
|
|
4010
|
+
};
|
|
4011
|
+
}
|
|
4012
|
+
/**
|
|
4013
|
+
* Deep merge (`patch` wins): nested plain objects are merged key-by-key; every
|
|
4014
|
+
* other value (arrays, scalars) is replaced wholesale. The policy for a
|
|
4015
|
+
* feature whose contribution interleaves with user-authored siblings at any
|
|
4016
|
+
* depth (e.g. permissions overlays onto `approvals`/`security` structures, or
|
|
4017
|
+
* per-provider option tables) — nested sibling keys are preserved by
|
|
4018
|
+
* construction instead of by per-tool re-implementation. Prototype-pollution
|
|
4019
|
+
* keys are dropped.
|
|
4020
|
+
*/
|
|
4021
|
+
function mergeSharedConfigDeep({ base, patch }) {
|
|
4022
|
+
const result = { ...base };
|
|
4023
|
+
for (const [key, patchValue] of Object.entries(patch)) {
|
|
4024
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
4025
|
+
const baseValue = result[key];
|
|
4026
|
+
if (isPlainObject(baseValue) && isPlainObject(patchValue)) result[key] = mergeSharedConfigDeep({
|
|
4027
|
+
base: baseValue,
|
|
4028
|
+
patch: patchValue
|
|
4029
|
+
});
|
|
4030
|
+
else result[key] = sanitizeSharedConfigValue(patchValue);
|
|
4031
|
+
}
|
|
4032
|
+
return result;
|
|
4033
|
+
}
|
|
4034
|
+
const CLAUDE_SETTINGS_SHARED_FILE_KEY = ".claude/settings.json";
|
|
4035
|
+
const HERMES_CONFIG_SHARED_FILE_KEY = ".hermes/config.yaml";
|
|
4036
|
+
const TAKT_CONFIG_SHARED_FILE_KEY = ".takt/config.yaml";
|
|
4037
|
+
/**
|
|
4038
|
+
* Who owns what in each gateway-managed shared config file, and which policy
|
|
4039
|
+
* resolves conflicts. Keys are `dir/file` tokens matching
|
|
4040
|
+
* `deriveSharedFileWriters()`; a test keeps each entry's feature set in
|
|
4041
|
+
* lock-step with the writers derived from the processor registry, so an
|
|
4042
|
+
* undeclared writer fails CI instead of merging by accident.
|
|
4043
|
+
*/
|
|
4044
|
+
const SHARED_CONFIG_OWNERSHIP = {
|
|
4045
|
+
[CLAUDE_SETTINGS_SHARED_FILE_KEY]: {
|
|
4046
|
+
format: "json",
|
|
4047
|
+
features: {
|
|
4048
|
+
ignore: {
|
|
4049
|
+
kind: "custom",
|
|
4050
|
+
policyFunction: "applyIgnoreReadDenies"
|
|
4051
|
+
},
|
|
4052
|
+
hooks: {
|
|
4053
|
+
kind: "replace-owned-keys",
|
|
4054
|
+
ownedKeys: ["hooks"]
|
|
4055
|
+
},
|
|
4056
|
+
permissions: {
|
|
4057
|
+
kind: "custom",
|
|
4058
|
+
policyFunction: "applyPermissions"
|
|
4059
|
+
}
|
|
4060
|
+
}
|
|
4061
|
+
},
|
|
4062
|
+
[HERMES_CONFIG_SHARED_FILE_KEY]: {
|
|
4063
|
+
format: "yaml",
|
|
4064
|
+
features: {
|
|
4065
|
+
subagents: {
|
|
4066
|
+
kind: "replace-owned-keys",
|
|
4067
|
+
ownedKeys: ["plugins"]
|
|
4068
|
+
},
|
|
4069
|
+
mcp: {
|
|
4070
|
+
kind: "replace-owned-keys",
|
|
4071
|
+
ownedKeys: ["mcp_servers"]
|
|
4072
|
+
},
|
|
4073
|
+
hooks: {
|
|
4074
|
+
kind: "replace-owned-keys",
|
|
4075
|
+
ownedKeys: ["hooks"]
|
|
4076
|
+
},
|
|
4077
|
+
permissions: {
|
|
4078
|
+
kind: "deep-merge",
|
|
4079
|
+
replaceKeys: ["permissions"]
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
},
|
|
4083
|
+
[TAKT_CONFIG_SHARED_FILE_KEY]: {
|
|
4084
|
+
format: "yaml",
|
|
4085
|
+
invalidRootPolicy: "error",
|
|
4086
|
+
features: {
|
|
4087
|
+
mcp: {
|
|
4088
|
+
kind: "replace-owned-keys",
|
|
4089
|
+
ownedKeys: [TAKT_WORKFLOW_MCP_SERVERS_KEY]
|
|
4090
|
+
},
|
|
4091
|
+
permissions: { kind: "deep-merge" }
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
4094
|
+
};
|
|
4095
|
+
/**
|
|
4096
|
+
* Execute a feature's declared write to a gateway-managed shared file: parse
|
|
4097
|
+
* the existing content, merge the patch under the feature's declared policy,
|
|
4098
|
+
* and serialize. Throws when the file or feature is undeclared, when a
|
|
4099
|
+
* `replace-owned-keys` patch strays outside its owned keys, or when the
|
|
4100
|
+
* feature's policy is `custom` (those calls go to the named policy function
|
|
4101
|
+
* instead).
|
|
4102
|
+
*/
|
|
4103
|
+
function applySharedConfigPatch({ fileKey, feature, existingContent, patch, filePath }) {
|
|
4104
|
+
const declaration = SHARED_CONFIG_OWNERSHIP[fileKey];
|
|
4105
|
+
if (!declaration) throw new Error(`Shared config file '${fileKey}' has no SHARED_CONFIG_OWNERSHIP declaration; declare its writers and policies before writing it through the gateway.`);
|
|
4106
|
+
const policy = declaration.features[feature];
|
|
4107
|
+
if (!policy) throw new Error(`Feature '${feature}' declares no ownership of '${fileKey}'; add it to SHARED_CONFIG_OWNERSHIP before writing.`);
|
|
4108
|
+
if (policy.kind === "custom") throw new Error(`Feature '${feature}' writes '${fileKey}' through its dedicated policy function '${policy.policyFunction}' in shared-config-gateway.ts, not applySharedConfigPatch.`);
|
|
4109
|
+
const base = parseSharedConfig({
|
|
4110
|
+
format: declaration.format,
|
|
4111
|
+
fileContent: existingContent,
|
|
4112
|
+
filePath,
|
|
4113
|
+
...declaration.invalidRootPolicy !== void 0 && { invalidRootPolicy: declaration.invalidRootPolicy }
|
|
4114
|
+
});
|
|
4115
|
+
if (policy.kind === "replace-owned-keys") {
|
|
4116
|
+
const unowned = Object.keys(patch).filter((key) => !policy.ownedKeys.includes(key));
|
|
4117
|
+
if (unowned.length > 0) throw new Error(`Feature '${feature}' tried to write undeclared keys [${unowned.join(", ")}] to '${fileKey}'; extend its ownedKeys declaration if that ownership is intended.`);
|
|
4118
|
+
return stringifySharedConfig({
|
|
4119
|
+
format: declaration.format,
|
|
4120
|
+
document: mergeSharedConfigShallow({
|
|
4121
|
+
base,
|
|
4122
|
+
patch
|
|
4123
|
+
})
|
|
4124
|
+
});
|
|
4125
|
+
}
|
|
4126
|
+
const merged = mergeSharedConfigDeep({
|
|
4127
|
+
base,
|
|
4128
|
+
patch
|
|
4129
|
+
});
|
|
4130
|
+
for (const key of policy.replaceKeys ?? []) if (patch[key] !== void 0) merged[key] = sanitizeSharedConfigValue(patch[key]);
|
|
4131
|
+
return stringifySharedConfig({
|
|
4132
|
+
format: declaration.format,
|
|
4133
|
+
document: merged
|
|
4134
|
+
});
|
|
4135
|
+
}
|
|
4136
|
+
const READ_TOOL_NAME = "Read";
|
|
4137
|
+
const isReadDenyEntry = (entry) => entry.startsWith(`${READ_TOOL_NAME}(`) && entry.endsWith(")");
|
|
4138
|
+
const buildReadDenyEntry = (pattern) => `${READ_TOOL_NAME}(${pattern})`;
|
|
4139
|
+
const parsePermissionsBlock = (settings) => {
|
|
4140
|
+
const permissions = settings.permissions ?? {};
|
|
4141
|
+
return {
|
|
4142
|
+
allow: permissions.allow ?? [],
|
|
4143
|
+
ask: permissions.ask ?? [],
|
|
4144
|
+
deny: permissions.deny ?? []
|
|
4145
|
+
};
|
|
4146
|
+
};
|
|
4147
|
+
const withPermissions = (settings, next) => {
|
|
4148
|
+
const permissions = { ...settings.permissions };
|
|
4149
|
+
const assign = (key, values) => {
|
|
4150
|
+
if (values.length > 0) permissions[key] = values;
|
|
4151
|
+
else delete permissions[key];
|
|
4152
|
+
};
|
|
4153
|
+
assign("allow", next.allow);
|
|
4154
|
+
assign("ask", next.ask);
|
|
4155
|
+
assign("deny", next.deny);
|
|
4156
|
+
return {
|
|
4157
|
+
...settings,
|
|
4158
|
+
permissions
|
|
4159
|
+
};
|
|
4160
|
+
};
|
|
4161
|
+
const applyIgnoreReadDenies = (params) => {
|
|
4162
|
+
const { settings, readDenies } = params;
|
|
4163
|
+
const current = parsePermissionsBlock(settings);
|
|
4164
|
+
const preservedDeny = current.deny.filter((entry) => !isReadDenyEntry(entry) || readDenies.includes(entry));
|
|
4165
|
+
return withPermissions(settings, {
|
|
4166
|
+
allow: current.allow,
|
|
4167
|
+
ask: current.ask,
|
|
4168
|
+
deny: uniq([...preservedDeny, ...readDenies].toSorted())
|
|
4169
|
+
});
|
|
4170
|
+
};
|
|
4171
|
+
const applyPermissions = (params) => {
|
|
4172
|
+
const { settings, managedToolNames, toolNameOf, allow, ask, deny, logger } = params;
|
|
4173
|
+
const current = parsePermissionsBlock(settings);
|
|
4174
|
+
const keepUnmanaged = (entries) => entries.filter((entry) => !managedToolNames.has(toolNameOf(entry)));
|
|
4175
|
+
if (logger && managedToolNames.has(READ_TOOL_NAME)) {
|
|
4176
|
+
const overwrittenReadDenies = current.deny.filter((entry) => toolNameOf(entry) === READ_TOOL_NAME);
|
|
4177
|
+
if (overwrittenReadDenies.length > 0) logger.warn(`Permissions feature manages '${READ_TOOL_NAME}' tool and will overwrite ${overwrittenReadDenies.length} existing ${READ_TOOL_NAME} deny entries. Permissions take precedence.`);
|
|
4178
|
+
}
|
|
4179
|
+
return withPermissions(settings, {
|
|
4180
|
+
allow: uniq([...keepUnmanaged(current.allow), ...allow].toSorted()),
|
|
4181
|
+
ask: uniq([...keepUnmanaged(current.ask), ...ask].toSorted()),
|
|
4182
|
+
deny: uniq([...keepUnmanaged(current.deny), ...deny].toSorted())
|
|
4183
|
+
});
|
|
4184
|
+
};
|
|
4185
|
+
//#endregion
|
|
3895
4186
|
//#region src/features/opencode-config.ts
|
|
3896
4187
|
/**
|
|
3897
4188
|
* Reads and parses the OpenCode config (`opencode.jsonc` preferred, then
|
|
@@ -3916,9 +4207,10 @@ async function readOpencodeConfig({ outputRoot, global = false }) {
|
|
|
3916
4207
|
});
|
|
3917
4208
|
const fileContent = await readFileContentOrNull(join(configDir, "opencode.jsonc")) ?? await readFileContentOrNull(join(configDir, "opencode.json"));
|
|
3918
4209
|
if (!fileContent) return {};
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
4210
|
+
return parseSharedConfig({
|
|
4211
|
+
format: "jsonc",
|
|
4212
|
+
fileContent
|
|
4213
|
+
});
|
|
3922
4214
|
}
|
|
3923
4215
|
/**
|
|
3924
4216
|
* Narrows an unknown value to a plain record of entries keyed by name, as used
|
|
@@ -4827,32 +5119,6 @@ var RovodevPromptsManifest = class extends ToolFile {
|
|
|
4827
5119
|
}
|
|
4828
5120
|
};
|
|
4829
5121
|
//#endregion
|
|
4830
|
-
//#region src/constants/takt-paths.ts
|
|
4831
|
-
const TAKT_DIR = ".takt";
|
|
4832
|
-
const TAKT_FACETS_SUBDIR = "facets";
|
|
4833
|
-
const TAKT_FACETS_DIR_PATH = join(TAKT_DIR, TAKT_FACETS_SUBDIR);
|
|
4834
|
-
const TAKT_RULES_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "policies");
|
|
4835
|
-
const TAKT_COMMANDS_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "instructions");
|
|
4836
|
-
const TAKT_SKILLS_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "knowledge");
|
|
4837
|
-
const TAKT_SUBAGENTS_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "personas");
|
|
4838
|
-
const TAKT_OUTPUT_CONTRACTS_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "output-contracts");
|
|
4839
|
-
const TAKT_RULE_OVERVIEW_FILE_NAME = "overview.md";
|
|
4840
|
-
/**
|
|
4841
|
-
* Takt's shared config file. Lives at `.takt/config.yaml` (project) and
|
|
4842
|
-
* `~/.takt/config.yaml` (global); it holds the active provider, provider
|
|
4843
|
-
* profiles (including permission modes), and other Takt settings.
|
|
4844
|
-
* @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
|
|
4845
|
-
*/
|
|
4846
|
-
const TAKT_CONFIG_FILE_NAME = "config.yaml";
|
|
4847
|
-
/**
|
|
4848
|
-
* Top-level key in Takt's `config.yaml` holding the workflow MCP transport
|
|
4849
|
-
* allowlist (`stdio` / `sse` / `http` booleans). Takt is default-deny: a
|
|
4850
|
-
* transport must be explicitly enabled here before any workflow-defined MCP
|
|
4851
|
-
* server using it is permitted to run.
|
|
4852
|
-
* @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
|
|
4853
|
-
*/
|
|
4854
|
-
const TAKT_WORKFLOW_MCP_SERVERS_KEY = "workflow_mcp_servers";
|
|
4855
|
-
//#endregion
|
|
4856
5122
|
//#region src/features/takt-shared.ts
|
|
4857
5123
|
/**
|
|
4858
5124
|
* Shared utilities for all TAKT-* tool file classes.
|
|
@@ -5688,6 +5954,8 @@ const DEEPAGENTS_HOOK_EVENTS = [
|
|
|
5688
5954
|
"sessionEnd",
|
|
5689
5955
|
"beforeSubmitPrompt",
|
|
5690
5956
|
"permissionRequest",
|
|
5957
|
+
"preToolUse",
|
|
5958
|
+
"postToolUse",
|
|
5691
5959
|
"postToolUseFailure",
|
|
5692
5960
|
"stop",
|
|
5693
5961
|
"preCompact",
|
|
@@ -5713,7 +5981,13 @@ const CODEXCLI_HOOK_EVENTS = [
|
|
|
5713
5981
|
* Goose adopts the Open Plugins hooks spec: each plugin's `hooks/hooks.json`
|
|
5714
5982
|
* maps PascalCase event names to matcher/handler arrays. Every Goose event has a
|
|
5715
5983
|
* 1:1 canonical equivalent, so no new canonical events are required.
|
|
5716
|
-
*
|
|
5984
|
+
*
|
|
5985
|
+
* Goose's `HookEvent` enum defines exactly these 11 events (v1.41.0). Notably it
|
|
5986
|
+
* has NO `SubagentStart`/`SubagentStop` arms — emitting them would write keys
|
|
5987
|
+
* Goose silently ignores, so `subagentStart`/`subagentStop` are intentionally
|
|
5988
|
+
* excluded here and from `CANONICAL_TO_GOOSE_EVENT_NAMES`.
|
|
5989
|
+
* @see https://github.com/block/goose/blob/v1.41.0/crates/goose/src/hooks/mod.rs
|
|
5990
|
+
* @see https://block.github.io/goose/docs/guides/context-engineering/hooks/
|
|
5717
5991
|
*/
|
|
5718
5992
|
const GOOSE_HOOK_EVENTS = [
|
|
5719
5993
|
"sessionStart",
|
|
@@ -5726,9 +6000,7 @@ const GOOSE_HOOK_EVENTS = [
|
|
|
5726
6000
|
"beforeReadFile",
|
|
5727
6001
|
"afterFileEdit",
|
|
5728
6002
|
"beforeShellExecution",
|
|
5729
|
-
"afterShellExecution"
|
|
5730
|
-
"subagentStart",
|
|
5731
|
-
"subagentStop"
|
|
6003
|
+
"afterShellExecution"
|
|
5732
6004
|
];
|
|
5733
6005
|
/** Hook events supported by Kiro CLI. */
|
|
5734
6006
|
const KIRO_HOOK_EVENTS = [
|
|
@@ -6181,9 +6453,7 @@ const CANONICAL_TO_GOOSE_EVENT_NAMES = {
|
|
|
6181
6453
|
beforeReadFile: "BeforeReadFile",
|
|
6182
6454
|
afterFileEdit: "AfterFileEdit",
|
|
6183
6455
|
beforeShellExecution: "BeforeShellExecution",
|
|
6184
|
-
afterShellExecution: "AfterShellExecution"
|
|
6185
|
-
subagentStart: "SubagentStart",
|
|
6186
|
-
subagentStop: "SubagentStop"
|
|
6456
|
+
afterShellExecution: "AfterShellExecution"
|
|
6187
6457
|
};
|
|
6188
6458
|
/**
|
|
6189
6459
|
* Map Goose PascalCase event names to canonical camelCase.
|
|
@@ -6197,6 +6467,8 @@ const CANONICAL_TO_DEEPAGENTS_EVENT_NAMES = {
|
|
|
6197
6467
|
sessionEnd: "session.end",
|
|
6198
6468
|
beforeSubmitPrompt: "user.prompt",
|
|
6199
6469
|
permissionRequest: "permission.request",
|
|
6470
|
+
preToolUse: "tool.use",
|
|
6471
|
+
postToolUse: "tool.result",
|
|
6200
6472
|
postToolUseFailure: "tool.error",
|
|
6201
6473
|
stop: "task.complete",
|
|
6202
6474
|
preCompact: "context.compact",
|
|
@@ -6324,42 +6596,6 @@ const CANONICAL_TO_REASONIX_EVENT_NAMES = {
|
|
|
6324
6596
|
*/
|
|
6325
6597
|
const REASONIX_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_REASONIX_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
6326
6598
|
//#endregion
|
|
6327
|
-
//#region src/utils/prototype-pollution.ts
|
|
6328
|
-
/**
|
|
6329
|
-
* Keys that, if walked into when constructing or merging objects from
|
|
6330
|
-
* untrusted input, can mutate `Object.prototype` (or otherwise the prototype
|
|
6331
|
-
* chain) and propagate state to every other object in the runtime. Any code
|
|
6332
|
-
* that copies arbitrary user-supplied keys into a fresh object — frontmatter
|
|
6333
|
-
* parsing, MCP config conversion, settings round-trip — should skip these.
|
|
6334
|
-
*/
|
|
6335
|
-
const PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
|
|
6336
|
-
"__proto__",
|
|
6337
|
-
"constructor",
|
|
6338
|
-
"prototype"
|
|
6339
|
-
]);
|
|
6340
|
-
function isPrototypePollutionKey(key) {
|
|
6341
|
-
return PROTOTYPE_POLLUTION_KEYS.has(key);
|
|
6342
|
-
}
|
|
6343
|
-
/**
|
|
6344
|
-
* Returns a shallow copy of a record's own entries with every
|
|
6345
|
-
* prototype-pollution key (`__proto__`, `constructor`, `prototype`) dropped.
|
|
6346
|
-
*
|
|
6347
|
-
* Use when copying a nested, user-supplied string map — an MCP server's `env`
|
|
6348
|
-
* or `headers` table — into freshly generated config. Carrying such a map by
|
|
6349
|
-
* reference, or re-assigning its keys via bracket notation, would let a literal
|
|
6350
|
-
* `__proto__` key ride along (and re-assigning it would mutate the target's
|
|
6351
|
-
* prototype). Walking the entries through this helper severs that path while
|
|
6352
|
-
* preserving every legitimate key.
|
|
6353
|
-
*/
|
|
6354
|
-
function omitPrototypePollutionKeys(record) {
|
|
6355
|
-
const sanitized = {};
|
|
6356
|
-
for (const [key, value] of Object.entries(record)) {
|
|
6357
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
6358
|
-
sanitized[key] = value;
|
|
6359
|
-
}
|
|
6360
|
-
return sanitized;
|
|
6361
|
-
}
|
|
6362
|
-
//#endregion
|
|
6363
6599
|
//#region src/features/hooks/tool-hooks-converter.ts
|
|
6364
6600
|
function isToolMatcherEntry(x) {
|
|
6365
6601
|
if (x === null || typeof x !== "object") return false;
|
|
@@ -7018,24 +7254,19 @@ var ClaudecodeHooks = class ClaudecodeHooks extends ToolHooks {
|
|
|
7018
7254
|
const paths = ClaudecodeHooks.getSettablePaths({ global });
|
|
7019
7255
|
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
7020
7256
|
const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
|
|
7021
|
-
let settings;
|
|
7022
|
-
try {
|
|
7023
|
-
settings = JSON.parse(existingContent);
|
|
7024
|
-
} catch (error) {
|
|
7025
|
-
throw new Error(`Failed to parse existing Claude settings at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
7026
|
-
}
|
|
7027
7257
|
const config = rulesyncHooks.getJson();
|
|
7028
|
-
const
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7258
|
+
const fileContent = applySharedConfigPatch({
|
|
7259
|
+
fileKey: CLAUDE_SETTINGS_SHARED_FILE_KEY,
|
|
7260
|
+
feature: "hooks",
|
|
7261
|
+
existingContent,
|
|
7262
|
+
patch: { hooks: canonicalToToolHooks({
|
|
7263
|
+
config,
|
|
7264
|
+
toolOverrideHooks: config.claudecode?.hooks,
|
|
7265
|
+
converterConfig: CLAUDE_CONVERTER_CONFIG,
|
|
7266
|
+
logger
|
|
7267
|
+
}) },
|
|
7268
|
+
filePath
|
|
7033
7269
|
});
|
|
7034
|
-
const merged = {
|
|
7035
|
-
...settings,
|
|
7036
|
-
hooks: claudeHooks
|
|
7037
|
-
};
|
|
7038
|
-
const fileContent = JSON.stringify(merged, null, 2);
|
|
7039
7270
|
return new ClaudecodeHooks({
|
|
7040
7271
|
outputRoot,
|
|
7041
7272
|
relativeDirPath: paths.relativeDirPath,
|
|
@@ -8191,7 +8422,7 @@ const GOOSE_CONVERTER_CONFIG = {
|
|
|
8191
8422
|
*
|
|
8192
8423
|
* The JSON shape matches Claude Code's: each PascalCase event maps to an array of
|
|
8193
8424
|
* `{ matcher, hooks: [{ type: "command", command }] }` entries.
|
|
8194
|
-
* @see https://
|
|
8425
|
+
* @see https://block.github.io/goose/docs/guides/context-engineering/hooks/
|
|
8195
8426
|
*/
|
|
8196
8427
|
var GooseHooks = class GooseHooks extends ToolHooks {
|
|
8197
8428
|
constructor(params) {
|
|
@@ -8242,7 +8473,7 @@ var GooseHooks = class GooseHooks extends ToolHooks {
|
|
|
8242
8473
|
throw new Error(`Failed to parse Goose hooks content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
8243
8474
|
}
|
|
8244
8475
|
const hooks = toolHooksToCanonical({
|
|
8245
|
-
hooks: parsed.hooks,
|
|
8476
|
+
hooks: parsed.hooks && typeof parsed.hooks === "object" && !Array.isArray(parsed.hooks) ? Object.fromEntries(Object.entries(parsed.hooks).filter(([eventName]) => Object.hasOwn(GOOSE_TO_CANONICAL_EVENT_NAMES, eventName))) : parsed.hooks,
|
|
8246
8477
|
converterConfig: GOOSE_CONVERTER_CONFIG
|
|
8247
8478
|
});
|
|
8248
8479
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
|
|
@@ -8267,37 +8498,6 @@ var GooseHooks = class GooseHooks extends ToolHooks {
|
|
|
8267
8498
|
}
|
|
8268
8499
|
};
|
|
8269
8500
|
//#endregion
|
|
8270
|
-
//#region src/features/hermes-config.ts
|
|
8271
|
-
function sanitizeHermesConfigValue(value) {
|
|
8272
|
-
if (Array.isArray(value)) return value.map(sanitizeHermesConfigValue);
|
|
8273
|
-
if (!isPlainObject(value)) return value;
|
|
8274
|
-
const sanitized = omitPrototypePollutionKeys(value);
|
|
8275
|
-
const result = {};
|
|
8276
|
-
for (const [key, nestedValue] of Object.entries(sanitized)) {
|
|
8277
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
8278
|
-
result[key] = sanitizeHermesConfigValue(nestedValue);
|
|
8279
|
-
}
|
|
8280
|
-
return result;
|
|
8281
|
-
}
|
|
8282
|
-
function parseHermesConfig(fileContent) {
|
|
8283
|
-
if (!fileContent.trim()) return {};
|
|
8284
|
-
const parsed = load(fileContent);
|
|
8285
|
-
if (isPlainObject(parsed)) return sanitizeHermesConfigValue(parsed);
|
|
8286
|
-
return {};
|
|
8287
|
-
}
|
|
8288
|
-
function stringifyHermesConfig(config) {
|
|
8289
|
-
return dump(config, {
|
|
8290
|
-
noRefs: true,
|
|
8291
|
-
sortKeys: false
|
|
8292
|
-
}).trimEnd() + "\n";
|
|
8293
|
-
}
|
|
8294
|
-
function mergeHermesConfig(fileContent, patch) {
|
|
8295
|
-
return stringifyHermesConfig({
|
|
8296
|
-
...parseHermesConfig(fileContent),
|
|
8297
|
-
...patch
|
|
8298
|
-
});
|
|
8299
|
-
}
|
|
8300
|
-
//#endregion
|
|
8301
8501
|
//#region src/features/hooks/hermesagent-hooks.ts
|
|
8302
8502
|
/**
|
|
8303
8503
|
* Canonical events that map to a Hermes tool-call event (`pre_tool_call` /
|
|
@@ -8425,10 +8625,21 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
|
|
|
8425
8625
|
return true;
|
|
8426
8626
|
}
|
|
8427
8627
|
setFileContent(fileContent) {
|
|
8428
|
-
this.fileContent =
|
|
8628
|
+
this.fileContent = applySharedConfigPatch({
|
|
8629
|
+
fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
|
|
8630
|
+
feature: "hooks",
|
|
8631
|
+
existingContent: fileContent,
|
|
8632
|
+
patch: parseSharedConfig({
|
|
8633
|
+
format: "yaml",
|
|
8634
|
+
fileContent: this.fileContent
|
|
8635
|
+
})
|
|
8636
|
+
});
|
|
8429
8637
|
}
|
|
8430
8638
|
toRulesyncHooks() {
|
|
8431
|
-
const hooks = hermesHooksToCanonical(
|
|
8639
|
+
const hooks = hermesHooksToCanonical(parseSharedConfig({
|
|
8640
|
+
format: "yaml",
|
|
8641
|
+
fileContent: this.getFileContent()
|
|
8642
|
+
}).hooks);
|
|
8432
8643
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
|
|
8433
8644
|
version: 1,
|
|
8434
8645
|
hooks
|
|
@@ -8438,11 +8649,14 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
|
|
|
8438
8649
|
const config = rulesyncHooks.getJson();
|
|
8439
8650
|
return new HermesagentHooks({
|
|
8440
8651
|
outputRoot,
|
|
8441
|
-
fileContent:
|
|
8442
|
-
|
|
8443
|
-
|
|
8444
|
-
|
|
8445
|
-
|
|
8652
|
+
fileContent: stringifySharedConfig({
|
|
8653
|
+
format: "yaml",
|
|
8654
|
+
document: { hooks: canonicalToHermesHooks({
|
|
8655
|
+
config,
|
|
8656
|
+
toolOverrideHooks: config.hermesagent?.hooks,
|
|
8657
|
+
logger
|
|
8658
|
+
}) }
|
|
8659
|
+
})
|
|
8446
8660
|
});
|
|
8447
8661
|
}
|
|
8448
8662
|
};
|
|
@@ -10462,66 +10676,6 @@ var AugmentcodeIgnore = class AugmentcodeIgnore extends ToolIgnore {
|
|
|
10462
10676
|
}
|
|
10463
10677
|
};
|
|
10464
10678
|
//#endregion
|
|
10465
|
-
//#region src/features/claudecode-settings-gateway.ts
|
|
10466
|
-
/**
|
|
10467
|
-
* Single owner of the `.claude/settings.json` `permissions` block, which both
|
|
10468
|
-
* `ignore` (writes `Read(...)` into `permissions.deny`) and `permissions`
|
|
10469
|
-
* (writes the whole `allow`/`ask`/`deny`) read-modify-write. The entry format,
|
|
10470
|
-
* the merge, and the cross-feature ownership rule (permissions' explicit `Read`
|
|
10471
|
-
* rules win over ignore-derived `Read` denies) used to be duplicated across both
|
|
10472
|
-
* feature files; they live here once so each feature just states its intent and
|
|
10473
|
-
* never reasons about the other's existence.
|
|
10474
|
-
*/
|
|
10475
|
-
const READ_TOOL_NAME = "Read";
|
|
10476
|
-
const isReadDenyEntry = (entry) => entry.startsWith(`${READ_TOOL_NAME}(`) && entry.endsWith(")");
|
|
10477
|
-
const buildReadDenyEntry = (pattern) => `${READ_TOOL_NAME}(${pattern})`;
|
|
10478
|
-
const parsePermissionsBlock = (settings) => {
|
|
10479
|
-
const permissions = settings.permissions ?? {};
|
|
10480
|
-
return {
|
|
10481
|
-
allow: permissions.allow ?? [],
|
|
10482
|
-
ask: permissions.ask ?? [],
|
|
10483
|
-
deny: permissions.deny ?? []
|
|
10484
|
-
};
|
|
10485
|
-
};
|
|
10486
|
-
const withPermissions = (settings, next) => {
|
|
10487
|
-
const permissions = { ...settings.permissions };
|
|
10488
|
-
const assign = (key, values) => {
|
|
10489
|
-
if (values.length > 0) permissions[key] = values;
|
|
10490
|
-
else delete permissions[key];
|
|
10491
|
-
};
|
|
10492
|
-
assign("allow", next.allow);
|
|
10493
|
-
assign("ask", next.ask);
|
|
10494
|
-
assign("deny", next.deny);
|
|
10495
|
-
return {
|
|
10496
|
-
...settings,
|
|
10497
|
-
permissions
|
|
10498
|
-
};
|
|
10499
|
-
};
|
|
10500
|
-
const applyIgnoreReadDenies = (params) => {
|
|
10501
|
-
const { settings, readDenies } = params;
|
|
10502
|
-
const current = parsePermissionsBlock(settings);
|
|
10503
|
-
const preservedDeny = current.deny.filter((entry) => !isReadDenyEntry(entry) || readDenies.includes(entry));
|
|
10504
|
-
return withPermissions(settings, {
|
|
10505
|
-
allow: current.allow,
|
|
10506
|
-
ask: current.ask,
|
|
10507
|
-
deny: uniq([...preservedDeny, ...readDenies].toSorted())
|
|
10508
|
-
});
|
|
10509
|
-
};
|
|
10510
|
-
const applyPermissions = (params) => {
|
|
10511
|
-
const { settings, managedToolNames, toolNameOf, allow, ask, deny, logger } = params;
|
|
10512
|
-
const current = parsePermissionsBlock(settings);
|
|
10513
|
-
const keepUnmanaged = (entries) => entries.filter((entry) => !managedToolNames.has(toolNameOf(entry)));
|
|
10514
|
-
if (logger && managedToolNames.has(READ_TOOL_NAME)) {
|
|
10515
|
-
const overwrittenReadDenies = current.deny.filter((entry) => toolNameOf(entry) === READ_TOOL_NAME);
|
|
10516
|
-
if (overwrittenReadDenies.length > 0) logger.warn(`Permissions feature manages '${READ_TOOL_NAME}' tool and will overwrite ${overwrittenReadDenies.length} existing ${READ_TOOL_NAME} deny entries. Permissions take precedence.`);
|
|
10517
|
-
}
|
|
10518
|
-
return withPermissions(settings, {
|
|
10519
|
-
allow: uniq([...keepUnmanaged(current.allow), ...allow].toSorted()),
|
|
10520
|
-
ask: uniq([...keepUnmanaged(current.ask), ...ask].toSorted()),
|
|
10521
|
-
deny: uniq([...keepUnmanaged(current.deny), ...deny].toSorted())
|
|
10522
|
-
});
|
|
10523
|
-
};
|
|
10524
|
-
//#endregion
|
|
10525
10679
|
//#region src/features/ignore/claudecode-ignore.ts
|
|
10526
10680
|
const DEFAULT_FILE_MODE = "shared";
|
|
10527
10681
|
/**
|
|
@@ -11944,7 +12098,6 @@ var AntigravityIdeMcp = class extends AntigravityMcp {
|
|
|
11944
12098
|
};
|
|
11945
12099
|
//#endregion
|
|
11946
12100
|
//#region src/features/mcp/augmentcode-mcp.ts
|
|
11947
|
-
const AUGMENTCODE_GLOBAL_ONLY_MESSAGE = "AugmentCode MCP is global-only; use --global to sync ~/.augment/settings.json";
|
|
11948
12101
|
function parseAugmentcodeSettings(fileContent, relativeDirPath, relativeFilePath) {
|
|
11949
12102
|
const configPath = join(relativeDirPath, relativeFilePath);
|
|
11950
12103
|
let parsed;
|
|
@@ -11959,13 +12112,15 @@ function parseAugmentcodeSettings(fileContent, relativeDirPath, relativeFilePath
|
|
|
11959
12112
|
/**
|
|
11960
12113
|
* AugmentCode (Auggie CLI) MCP servers.
|
|
11961
12114
|
*
|
|
11962
|
-
* MCP servers are persisted in the shared
|
|
11963
|
-
*
|
|
11964
|
-
*
|
|
12115
|
+
* MCP servers are persisted in the shared settings file `.augment/settings.json`
|
|
12116
|
+
* at either scope: the committed workspace file for team-shared servers (project)
|
|
12117
|
+
* or `~/.augment/settings.json` (global). That same file also holds `hooks` and
|
|
11965
12118
|
* `toolPermissions`, so generation merges the `mcpServers` block into the
|
|
11966
|
-
* existing settings instead of overwriting it, and the file is never deleted.
|
|
12119
|
+
* existing settings instead of overwriting it, and the file is never deleted. On
|
|
12120
|
+
* project-scope import the gitignored `.augment/settings.local.json` overrides
|
|
12121
|
+
* file is overlaid on top (the same layering the hooks/permissions adapters use).
|
|
11967
12122
|
*
|
|
11968
|
-
* @see https://docs.augmentcode.com/cli/
|
|
12123
|
+
* @see https://docs.augmentcode.com/cli/config
|
|
11969
12124
|
*/
|
|
11970
12125
|
var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
|
|
11971
12126
|
json;
|
|
@@ -11987,9 +12142,14 @@ var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
|
|
|
11987
12142
|
};
|
|
11988
12143
|
}
|
|
11989
12144
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
11990
|
-
if (!global) throw new Error(AUGMENTCODE_GLOBAL_ONLY_MESSAGE);
|
|
11991
12145
|
const paths = this.getSettablePaths({ global });
|
|
11992
|
-
const json = parseAugmentcodeSettings(await
|
|
12146
|
+
const json = parseAugmentcodeSettings(await readAugmentcodeSettingsWithLocalOverlay({
|
|
12147
|
+
outputRoot,
|
|
12148
|
+
relativeDirPath: paths.relativeDirPath,
|
|
12149
|
+
baseFileName: paths.relativeFilePath,
|
|
12150
|
+
baseFallbackContent: "{}",
|
|
12151
|
+
includeLocalOverlay: !global
|
|
12152
|
+
}), paths.relativeDirPath, paths.relativeFilePath);
|
|
11993
12153
|
const newJson = {
|
|
11994
12154
|
...json,
|
|
11995
12155
|
mcpServers: json.mcpServers ?? {}
|
|
@@ -12004,7 +12164,6 @@ var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
|
|
|
12004
12164
|
});
|
|
12005
12165
|
}
|
|
12006
12166
|
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
|
|
12007
|
-
if (!global) throw new Error(AUGMENTCODE_GLOBAL_ONLY_MESSAGE);
|
|
12008
12167
|
const paths = this.getSettablePaths({ global });
|
|
12009
12168
|
const merged = {
|
|
12010
12169
|
...parseAugmentcodeSettings(await readOrInitializeFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath), JSON.stringify({}, null, 2)), paths.relativeDirPath, paths.relativeFilePath),
|
|
@@ -12279,6 +12438,38 @@ const RULESYNC_TO_CODEX_FIELD_MAP = {
|
|
|
12279
12438
|
envVars: "env_vars"
|
|
12280
12439
|
};
|
|
12281
12440
|
const MAX_REMOVE_EMPTY_ENTRIES_DEPTH$1 = 32;
|
|
12441
|
+
/**
|
|
12442
|
+
* Translate a server's `oauth` table from the canonical rulesync shape (Claude
|
|
12443
|
+
* Code style camelCase) into the shape Codex CLI understands. Codex expects the
|
|
12444
|
+
* OAuth client id under snake_case `client_id`; without it `codex mcp login`
|
|
12445
|
+
* falls back to dynamic client registration and fails for providers that do not
|
|
12446
|
+
* support it (e.g. Slack, see #2158). The canonical `clientId` is kept alongside
|
|
12447
|
+
* the added `client_id` so tools that read the camelCase shape keep working and
|
|
12448
|
+
* the round-trip stays stable.
|
|
12449
|
+
*/
|
|
12450
|
+
function mapOauthToCodex(oauth) {
|
|
12451
|
+
const result = omitPrototypePollutionKeys(oauth);
|
|
12452
|
+
if (typeof oauth["clientId"] === "string" && !("client_id" in result)) result["client_id"] = oauth["clientId"];
|
|
12453
|
+
return result;
|
|
12454
|
+
}
|
|
12455
|
+
/**
|
|
12456
|
+
* Reverse of {@link mapOauthToCodex}: collapse Codex's `oauth.client_id` back to
|
|
12457
|
+
* the canonical `clientId` on import. When both keys are present (the shape
|
|
12458
|
+
* rulesync itself emits) the canonical `clientId` wins and `client_id` is
|
|
12459
|
+
* dropped so a subsequent generate does not accumulate duplicates.
|
|
12460
|
+
*/
|
|
12461
|
+
function mapOauthFromCodex(oauth) {
|
|
12462
|
+
const result = {};
|
|
12463
|
+
for (const [key, value] of Object.entries(oauth)) {
|
|
12464
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
12465
|
+
if (key === "client_id") {
|
|
12466
|
+
if (!("clientId" in oauth)) result["clientId"] = value;
|
|
12467
|
+
continue;
|
|
12468
|
+
}
|
|
12469
|
+
result[key] = value;
|
|
12470
|
+
}
|
|
12471
|
+
return result;
|
|
12472
|
+
}
|
|
12282
12473
|
function convertFromCodexFormat(codexMcp) {
|
|
12283
12474
|
const result = {};
|
|
12284
12475
|
for (const [name, config] of Object.entries(codexMcp)) {
|
|
@@ -12288,7 +12479,8 @@ function convertFromCodexFormat(codexMcp) {
|
|
|
12288
12479
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
12289
12480
|
if (key === "enabled") {
|
|
12290
12481
|
if (value === false) converted["disabled"] = true;
|
|
12291
|
-
} else if (
|
|
12482
|
+
} else if (key === "oauth" && isRecord(value)) converted[key] = mapOauthFromCodex(value);
|
|
12483
|
+
else if (Object.hasOwn(CODEX_TO_RULESYNC_FIELD_MAP, key)) {
|
|
12292
12484
|
const mappedKey = CODEX_TO_RULESYNC_FIELD_MAP[key];
|
|
12293
12485
|
if (mappedKey) if (isStringArray(value)) converted[mappedKey] = value;
|
|
12294
12486
|
else warnWithFallback(void 0, `Ignored malformed array for ${key} in MCP server ${name}`);
|
|
@@ -12308,7 +12500,8 @@ function convertToCodexFormat(mcpServers) {
|
|
|
12308
12500
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
12309
12501
|
if (key === "disabled") {
|
|
12310
12502
|
if (value === true) converted["enabled"] = false;
|
|
12311
|
-
} else if (
|
|
12503
|
+
} else if (key === "oauth" && isRecord(value)) converted[key] = mapOauthToCodex(value);
|
|
12504
|
+
else if (Object.hasOwn(RULESYNC_TO_CODEX_FIELD_MAP, key)) {
|
|
12312
12505
|
const mappedKey = RULESYNC_TO_CODEX_FIELD_MAP[key];
|
|
12313
12506
|
if (mappedKey) if (isStringArray(value)) converted[mappedKey] = value;
|
|
12314
12507
|
else warnWithFallback(void 0, `[CodexCliMcp] Skipping invalid value type for mapped key '${key}': expected string array, got ${typeof value}`);
|
|
@@ -13524,10 +13717,12 @@ function resolveHermesTimeout(config) {
|
|
|
13524
13717
|
*
|
|
13525
13718
|
* Hermes is close to the MCP spec but not identical: `command` must be a single
|
|
13526
13719
|
* executable string (an array's tail folds into `args`), a server is disabled
|
|
13527
|
-
* via `enabled: false` (not the canonical `disabled: true`),
|
|
13528
|
-
*
|
|
13529
|
-
* `
|
|
13530
|
-
*
|
|
13720
|
+
* via `enabled: false` (not the canonical `disabled: true`), remote servers use
|
|
13721
|
+
* `url`/`headers`, and per-server tool scoping lives under a `tools: { include,
|
|
13722
|
+
* exclude }` block (from the canonical `enabledTools`/`disabledTools`). Only
|
|
13723
|
+
* fields Hermes understands are emitted, so the shared `config.yaml` is not
|
|
13724
|
+
* polluted with canonical-only aliases (`type`, `transport`, `httpUrl`,
|
|
13725
|
+
* `networkTimeout`, ...).
|
|
13531
13726
|
*/
|
|
13532
13727
|
function convertServerToHermes(config) {
|
|
13533
13728
|
const out = {};
|
|
@@ -13551,6 +13746,10 @@ function convertServerToHermes(config) {
|
|
|
13551
13746
|
if (config.disabled === true) out.enabled = false;
|
|
13552
13747
|
const timeout = resolveHermesTimeout(config);
|
|
13553
13748
|
if (timeout !== void 0) out.timeout = timeout;
|
|
13749
|
+
const tools = {};
|
|
13750
|
+
if (isStringArray(config.enabledTools)) tools.include = config.enabledTools;
|
|
13751
|
+
if (isStringArray(config.disabledTools)) tools.exclude = config.disabledTools;
|
|
13752
|
+
if (Object.keys(tools).length > 0) out.tools = tools;
|
|
13554
13753
|
return out;
|
|
13555
13754
|
}
|
|
13556
13755
|
/**
|
|
@@ -13592,6 +13791,10 @@ function convertFromHermesFormat(mcpServers) {
|
|
|
13592
13791
|
if (isPlainObject(config.headers)) server.headers = omitPrototypePollutionKeys(config.headers);
|
|
13593
13792
|
if (config.enabled === false) server.disabled = true;
|
|
13594
13793
|
if (typeof config.timeout === "number") server.networkTimeout = config.timeout;
|
|
13794
|
+
if (isRecord(config.tools)) {
|
|
13795
|
+
if (isStringArray(config.tools.include)) server.enabledTools = config.tools.include;
|
|
13796
|
+
if (isStringArray(config.tools.exclude)) server.disabledTools = config.tools.exclude;
|
|
13797
|
+
}
|
|
13595
13798
|
result[name] = server;
|
|
13596
13799
|
}
|
|
13597
13800
|
return result;
|
|
@@ -13610,7 +13813,10 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
|
|
|
13610
13813
|
config;
|
|
13611
13814
|
constructor(params) {
|
|
13612
13815
|
super(params);
|
|
13613
|
-
this.config = this.fileContent !== void 0 ?
|
|
13816
|
+
this.config = this.fileContent !== void 0 ? parseSharedConfig({
|
|
13817
|
+
format: "yaml",
|
|
13818
|
+
fileContent: this.fileContent
|
|
13819
|
+
}) : {};
|
|
13614
13820
|
}
|
|
13615
13821
|
getConfig() {
|
|
13616
13822
|
return this.config;
|
|
@@ -13619,9 +13825,17 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
|
|
|
13619
13825
|
return true;
|
|
13620
13826
|
}
|
|
13621
13827
|
setFileContent(fileContent) {
|
|
13622
|
-
const merged = mergeHermesMcpServers(
|
|
13828
|
+
const merged = mergeHermesMcpServers(parseSharedConfig({
|
|
13829
|
+
format: "yaml",
|
|
13830
|
+
fileContent
|
|
13831
|
+
}), isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
|
|
13623
13832
|
this.config = merged;
|
|
13624
|
-
super.setFileContent(
|
|
13833
|
+
super.setFileContent(applySharedConfigPatch({
|
|
13834
|
+
fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
|
|
13835
|
+
feature: "mcp",
|
|
13836
|
+
existingContent: fileContent,
|
|
13837
|
+
patch: { mcp_servers: merged.mcp_servers }
|
|
13838
|
+
}));
|
|
13625
13839
|
}
|
|
13626
13840
|
isDeletable() {
|
|
13627
13841
|
return false;
|
|
@@ -13648,12 +13862,21 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
|
|
|
13648
13862
|
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
|
|
13649
13863
|
if (!global) throw new Error(HERMESAGENT_GLOBAL_ONLY_MESSAGE);
|
|
13650
13864
|
const paths = this.getSettablePaths({ global });
|
|
13651
|
-
const
|
|
13865
|
+
const fileContent = await readOrInitializeFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath), "");
|
|
13866
|
+
const merged = mergeHermesMcpServers(parseSharedConfig({
|
|
13867
|
+
format: "yaml",
|
|
13868
|
+
fileContent
|
|
13869
|
+
}), convertToHermesFormat(rulesyncMcp.getMcpServers()));
|
|
13652
13870
|
return new HermesagentMcp({
|
|
13653
13871
|
outputRoot,
|
|
13654
13872
|
relativeDirPath: paths.relativeDirPath,
|
|
13655
13873
|
relativeFilePath: paths.relativeFilePath,
|
|
13656
|
-
fileContent:
|
|
13874
|
+
fileContent: applySharedConfigPatch({
|
|
13875
|
+
fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
|
|
13876
|
+
feature: "mcp",
|
|
13877
|
+
existingContent: fileContent,
|
|
13878
|
+
patch: { mcp_servers: merged.mcp_servers }
|
|
13879
|
+
}),
|
|
13657
13880
|
validate,
|
|
13658
13881
|
global
|
|
13659
13882
|
});
|
|
@@ -14818,28 +15041,6 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
|
|
|
14818
15041
|
}
|
|
14819
15042
|
};
|
|
14820
15043
|
//#endregion
|
|
14821
|
-
//#region src/features/shared/takt-config.ts
|
|
14822
|
-
/**
|
|
14823
|
-
* Parse a Takt `config.yaml` into a plain object, treating an empty file as `{}`.
|
|
14824
|
-
*
|
|
14825
|
-
* Shared by the Takt adapters that read-modify-write the same `config.yaml`
|
|
14826
|
-
* (mcp, permissions, ...). Uses `isPlainObject` (not `isRecord`) so class
|
|
14827
|
-
* instances are rejected for prototype-pollution hardening; a YAML mapping
|
|
14828
|
-
* always parses to a plain object.
|
|
14829
|
-
*/
|
|
14830
|
-
function parseTaktConfig(fileContent, relativeDirPath, relativeFilePath) {
|
|
14831
|
-
const configPath = join(relativeDirPath, relativeFilePath);
|
|
14832
|
-
let parsed;
|
|
14833
|
-
try {
|
|
14834
|
-
parsed = fileContent.trim() === "" ? {} : load(fileContent);
|
|
14835
|
-
} catch (error) {
|
|
14836
|
-
throw new Error(`Failed to parse Takt config at ${configPath}: ${formatError(error)}`, { cause: error });
|
|
14837
|
-
}
|
|
14838
|
-
if (parsed === void 0 || parsed === null) return {};
|
|
14839
|
-
if (!isPlainObject(parsed)) throw new Error(`Failed to parse Takt config at ${configPath}: expected a YAML mapping`);
|
|
14840
|
-
return parsed;
|
|
14841
|
-
}
|
|
14842
|
-
//#endregion
|
|
14843
15044
|
//#region src/features/mcp/takt-mcp.ts
|
|
14844
15045
|
/**
|
|
14845
15046
|
* MCP adapter for Takt (`.takt/config.yaml` project / `~/.takt/config.yaml`
|
|
@@ -14905,17 +15106,20 @@ var TaktMcp = class TaktMcp extends ToolMcp {
|
|
|
14905
15106
|
}
|
|
14906
15107
|
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
|
|
14907
15108
|
const paths = TaktMcp.getSettablePaths({ global });
|
|
14908
|
-
const
|
|
15109
|
+
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
15110
|
+
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
14909
15111
|
const allowlist = deriveTransportAllowlist(rulesyncMcp.getMcpServers());
|
|
14910
|
-
const merged = {
|
|
14911
|
-
...config,
|
|
14912
|
-
[TAKT_WORKFLOW_MCP_SERVERS_KEY]: allowlist
|
|
14913
|
-
};
|
|
14914
15112
|
return new TaktMcp({
|
|
14915
15113
|
outputRoot,
|
|
14916
15114
|
relativeDirPath: paths.relativeDirPath,
|
|
14917
15115
|
relativeFilePath: paths.relativeFilePath,
|
|
14918
|
-
fileContent:
|
|
15116
|
+
fileContent: applySharedConfigPatch({
|
|
15117
|
+
fileKey: TAKT_CONFIG_SHARED_FILE_KEY,
|
|
15118
|
+
feature: "mcp",
|
|
15119
|
+
existingContent,
|
|
15120
|
+
patch: { [TAKT_WORKFLOW_MCP_SERVERS_KEY]: allowlist },
|
|
15121
|
+
filePath
|
|
15122
|
+
}),
|
|
14919
15123
|
validate,
|
|
14920
15124
|
global
|
|
14921
15125
|
});
|
|
@@ -15360,7 +15564,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
15360
15564
|
["augmentcode", {
|
|
15361
15565
|
class: AugmentcodeMcp,
|
|
15362
15566
|
meta: {
|
|
15363
|
-
supportsProject:
|
|
15567
|
+
supportsProject: true,
|
|
15364
15568
|
supportsGlobal: true,
|
|
15365
15569
|
supportsEnabledTools: false,
|
|
15366
15570
|
supportsDisabledTools: false
|
|
@@ -15747,17 +15951,317 @@ const PermissionActionSchema = z.enum([
|
|
|
15747
15951
|
*/
|
|
15748
15952
|
const PermissionRulesSchema = z.record(z.string(), PermissionActionSchema);
|
|
15749
15953
|
/**
|
|
15954
|
+
* OpenCode-specific permission value. Unlike the shared canonical block, which
|
|
15955
|
+
* only accepts a pattern-to-action map, OpenCode also allows a bare action
|
|
15956
|
+
* string that applies to the whole category (e.g. `"external_directory": "deny"`).
|
|
15957
|
+
* This mirrors OpenCode's own permission schema in `opencode-permissions.ts`.
|
|
15958
|
+
*/
|
|
15959
|
+
const OpencodeOverridePermissionValueSchema = z.union([PermissionActionSchema, PermissionRulesSchema]);
|
|
15960
|
+
/**
|
|
15961
|
+
* Tool-scoped override block for OpenCode. Permission categories placed here
|
|
15962
|
+
* (e.g. OpenCode-only categories such as `external_directory`) are emitted only
|
|
15963
|
+
* into OpenCode's config and never leak into other tools' permission files. It
|
|
15964
|
+
* also lets a shared category be overridden with an OpenCode-specific value.
|
|
15965
|
+
* Kept `looseObject` so future OpenCode categories are accepted.
|
|
15966
|
+
*
|
|
15967
|
+
* @example
|
|
15968
|
+
* { "permission": { "external_directory": "deny", "webfetch": "allow" } }
|
|
15969
|
+
*/
|
|
15970
|
+
const OpencodePermissionsOverrideSchema = z.looseObject({ permission: z.optional(z.record(z.string(), OpencodeOverridePermissionValueSchema)) });
|
|
15971
|
+
/**
|
|
15972
|
+
* Tool-scoped override block for Hermes Agent. Keys placed here are deep-merged
|
|
15973
|
+
* into Hermes's `~/.hermes/config.yaml` and never leak into other tools' configs.
|
|
15974
|
+
* It carries Hermes-specific approval/security controls that have no canonical
|
|
15975
|
+
* permission category — e.g. `approvals` (`mode`, `cron_mode`, ...),
|
|
15976
|
+
* `security` (`allow_private_urls`, ...), `skills.write_approval`,
|
|
15977
|
+
* `memory.write_approval`. Kept `looseObject` (a verbatim passthrough) so any
|
|
15978
|
+
* current or future Hermes config key can be authored without modeling each one.
|
|
15979
|
+
*
|
|
15980
|
+
* @example
|
|
15981
|
+
* { "approvals": { "mode": "smart" }, "security": { "allow_private_urls": false } }
|
|
15982
|
+
*/
|
|
15983
|
+
const HermesPermissionsOverrideSchema = z.looseObject({});
|
|
15984
|
+
/**
|
|
15985
|
+
* Tool-scoped override block for Cline. Cline's `command-permissions.json`
|
|
15986
|
+
* carries a single global `allowRedirects` boolean (gates shell redirection
|
|
15987
|
+
* operators `>`/`>>`/`<`) that has no per-command dimension and therefore no
|
|
15988
|
+
* canonical permission category. Placing it here lets users author it
|
|
15989
|
+
* declaratively; it is emitted only into Cline's config. Kept `looseObject` so
|
|
15990
|
+
* future Cline-only knobs can be added.
|
|
15991
|
+
*
|
|
15992
|
+
* @example
|
|
15993
|
+
* { "allowRedirects": true }
|
|
15994
|
+
*/
|
|
15995
|
+
const ClinePermissionsOverrideSchema = z.looseObject({ allowRedirects: z.optional(z.boolean()) });
|
|
15996
|
+
/**
|
|
15997
|
+
* Tool-scoped override block for Kilo Code (an OpenCode fork). Kilo's permission
|
|
15998
|
+
* object is a free-form record with tool-specific keys that have no canonical
|
|
15999
|
+
* category — OpenCode-inherited ones (`external_directory`, `doom_loop`, `lsp`,
|
|
16000
|
+
* `question`, `todowrite`, `skill`, `task`, `list`) and Kilo-unique ones
|
|
16001
|
+
* (`agent_manager`, `notebook_read`, `notebook_edit`, `notebook_execute`,
|
|
16002
|
+
* `repo_clone`, `repo_overview`). Placing them here makes them authorable and
|
|
16003
|
+
* portable and keeps them out of other tools' configs. Mirrors the OpenCode
|
|
16004
|
+
* override; each value may be a bare action string or a pattern map.
|
|
16005
|
+
*
|
|
16006
|
+
* @example
|
|
16007
|
+
* { "permission": { "external_directory": "deny", "doom_loop": "ask" } }
|
|
16008
|
+
*/
|
|
16009
|
+
const KiloPermissionsOverrideSchema = z.looseObject({ permission: z.optional(z.record(z.string(), OpencodeOverridePermissionValueSchema)) });
|
|
16010
|
+
/**
|
|
16011
|
+
* Tool-scoped override block for Claude Code. Claude Code's `permissions` object
|
|
16012
|
+
* (in `.claude/settings.json`) carries non-list fields that have no canonical
|
|
16013
|
+
* permission category — `defaultMode` (the session-start permission mode) and
|
|
16014
|
+
* `additionalDirectories` (extra working directories) being the primary ones.
|
|
16015
|
+
* Fields placed under `claudecode.permissions` are merged into the settings
|
|
16016
|
+
* `permissions` object and emitted only for Claude Code, while the shared
|
|
16017
|
+
* `permission` block continues to drive the `allow`/`ask`/`deny` arrays. Kept a
|
|
16018
|
+
* `looseObject` passthrough so any current or future `permissions` field can be
|
|
16019
|
+
* authored without modeling each one; the managed `allow`/`ask`/`deny` arrays are
|
|
16020
|
+
* ignored here (rulesync owns them).
|
|
16021
|
+
*
|
|
16022
|
+
* @example
|
|
16023
|
+
* { "permissions": { "defaultMode": "acceptEdits", "additionalDirectories": ["../shared"] } }
|
|
16024
|
+
*/
|
|
16025
|
+
const ClaudecodePermissionsOverrideSchema = z.looseObject({ permissions: z.optional(z.looseObject({})) });
|
|
16026
|
+
/**
|
|
16027
|
+
* Tool-scoped override block for Mistral Vibe. Vibe's per-tool `BaseToolConfig`
|
|
16028
|
+
* carries a `sensitive_patterns` list — patterns that escalate to ASK even when
|
|
16029
|
+
* the tool's base permission is ALWAYS (allow). The canonical model can only set
|
|
16030
|
+
* a pattern to a single `allow`/`ask`/`deny`, so an "allow by default but ask on
|
|
16031
|
+
* these patterns" escalation cannot be expressed. Entries under
|
|
16032
|
+
* `vibe.permission.<category>.sensitive_patterns` carry that list per canonical
|
|
16033
|
+
* category; the shared `permission` block still drives the base permission and
|
|
16034
|
+
* allow/deny lists. Keyed by canonical category (e.g. `bash`, `edit`).
|
|
16035
|
+
*
|
|
16036
|
+
* @example
|
|
16037
|
+
* { "permission": { "bash": { "sensitive_patterns": ["rm *", "sudo *"] } } }
|
|
16038
|
+
*/
|
|
16039
|
+
const VibePermissionsOverrideSchema = z.looseObject({ permission: z.optional(z.record(z.string(), z.looseObject({ sensitive_patterns: z.optional(z.array(z.string())) }))) });
|
|
16040
|
+
/**
|
|
16041
|
+
* Tool-scoped override block for Cursor CLI. Cursor's `cli.json` carries scalar
|
|
16042
|
+
* autonomy settings with no canonical permission category — `approvalMode`
|
|
16043
|
+
* (`allowlist` | `auto-review` | `unrestricted`) and a `sandbox` object
|
|
16044
|
+
* (`mode`/`networkAccess`). Fields placed here are merged into the top-level of
|
|
16045
|
+
* `.cursor/cli.json` (project) / `~/.cursor/cli-config.json` (global) and emitted
|
|
16046
|
+
* only for Cursor, while the shared `permission` block continues to drive the
|
|
16047
|
+
* `permissions.allow`/`permissions.deny` arrays. Kept a `looseObject` so extra
|
|
16048
|
+
* `cli.json` keys can be authored (they are merged verbatim on generate);
|
|
16049
|
+
* `sandbox`'s accepted values are not documented so it passes through verbatim.
|
|
16050
|
+
* Note: only `approvalMode` and `sandbox` round-trip back on import — other keys
|
|
16051
|
+
* authored here reach `cli.json` on generate but are not re-extracted.
|
|
16052
|
+
*
|
|
16053
|
+
* @example
|
|
16054
|
+
* { "approvalMode": "auto-review" }
|
|
16055
|
+
*/
|
|
16056
|
+
const CursorPermissionsOverrideSchema = z.looseObject({
|
|
16057
|
+
approvalMode: z.optional(z.string()),
|
|
16058
|
+
sandbox: z.optional(z.looseObject({}))
|
|
16059
|
+
});
|
|
16060
|
+
/**
|
|
16061
|
+
* Tool-scoped override block for Qwen Code. Qwen's `settings.json` exposes
|
|
16062
|
+
* autonomy/sandbox controls with no canonical permission category — under
|
|
16063
|
+
* `tools` (`approvalMode` = plan/default/auto-edit/auto/yolo, `autoAccept`,
|
|
16064
|
+
* `sandbox`, `sandboxImage`, `disabled`) and `security` (`folderTrust`). Fields
|
|
16065
|
+
* placed here are merged into the matching `settings.json` group and emitted
|
|
16066
|
+
* only for Qwen, while the shared `permission` block continues to drive the
|
|
16067
|
+
* `permissions.allow`/`ask`/`deny` arrays. Kept `looseObject` (verbatim
|
|
16068
|
+
* passthrough) so any current or future `tools`/`security` key can be authored.
|
|
16069
|
+
*
|
|
16070
|
+
* @example
|
|
16071
|
+
* { "tools": { "approvalMode": "auto-edit" }, "security": { "folderTrust": { "enabled": true } } }
|
|
16072
|
+
*/
|
|
16073
|
+
const QwencodePermissionsOverrideSchema = z.looseObject({
|
|
16074
|
+
tools: z.optional(z.looseObject({})),
|
|
16075
|
+
security: z.optional(z.looseObject({}))
|
|
16076
|
+
});
|
|
16077
|
+
/**
|
|
16078
|
+
* Tool-scoped override block for Reasonix. Reasonix has security axes orthogonal
|
|
16079
|
+
* to per-tool allow/ask/deny with no canonical category — the `[sandbox]`
|
|
16080
|
+
* enforcement table (`workspace_root`, `allow_write`, `forbid_read`, `bash`,
|
|
16081
|
+
* `network`) and plan-mode read-only trust lists under `[agent]`
|
|
16082
|
+
* (`plan_mode_allowed_tools`, `plan_mode_read_only_commands`). Fields placed here
|
|
16083
|
+
* are merged into the matching `reasonix.toml` table and emitted only for
|
|
16084
|
+
* Reasonix, while the shared `permission` block continues to drive
|
|
16085
|
+
* `[permissions].allow`/`ask`/`deny`. Kept `looseObject` (verbatim passthrough).
|
|
16086
|
+
* Note: the whole `[sandbox]` table round-trips, but only the plan-mode keys are
|
|
16087
|
+
* re-extracted from `[agent]` on import — other `agent` keys authored here reach
|
|
16088
|
+
* `reasonix.toml` on generate but are not re-extracted back into the override.
|
|
16089
|
+
*
|
|
16090
|
+
* @example
|
|
16091
|
+
* { "sandbox": { "bash": "enforce", "network": false }, "agent": { "plan_mode_read_only_commands": ["gh pr diff"] } }
|
|
16092
|
+
*/
|
|
16093
|
+
const ReasonixPermissionsOverrideSchema = z.looseObject({
|
|
16094
|
+
sandbox: z.optional(z.looseObject({})),
|
|
16095
|
+
agent: z.optional(z.looseObject({}))
|
|
16096
|
+
});
|
|
16097
|
+
/**
|
|
16098
|
+
* Tool-scoped override block for Factory Droid. Factory Droid's `settings.json`
|
|
16099
|
+
* exposes security controls with no canonical per-command allow/ask/deny slot —
|
|
16100
|
+
* `commandBlocklist` (a hard-block tier that can never be approved, distinct from
|
|
16101
|
+
* an approvable `deny`), `networkPolicy` (`allowedIps`), `sandbox`
|
|
16102
|
+
* (`enabled`/`mode`/`filesystem`/`network`), `mcpPolicy`, `enableDroidShield`,
|
|
16103
|
+
* and autonomy settings (`sessionDefaultSettings`, `maxAutonomyLevel`,
|
|
16104
|
+
* `interactionMode`). Fields placed here are merged into `settings.json` and
|
|
16105
|
+
* emitted only for Factory Droid, while the shared `permission` block continues
|
|
16106
|
+
* to drive `commandAllowlist`/`commandDenylist`. Kept `looseObject` passthrough.
|
|
16107
|
+
*
|
|
16108
|
+
* @example
|
|
16109
|
+
* { "commandBlocklist": ["rm -rf /*"], "sandbox": { "enabled": true } }
|
|
16110
|
+
*/
|
|
16111
|
+
const FactorydroidPermissionsOverrideSchema = z.looseObject({ commandBlocklist: z.optional(z.array(z.string())) });
|
|
16112
|
+
/**
|
|
16113
|
+
* Tool-scoped override block for Warp. Warp's `[agents.profiles]` table exposes
|
|
16114
|
+
* file-read/read-only autonomy keys with no canonical per-command allow/ask/deny
|
|
16115
|
+
* slot — `agent_mode_coding_permissions`
|
|
16116
|
+
* (`always_ask_before_reading` | `always_allow_reading` | `allow_reading_specific_files`),
|
|
16117
|
+
* `agent_mode_coding_file_read_allowlist` (a path array), and
|
|
16118
|
+
* `agent_mode_execute_readonly_commands` (a read-only auto-execution boolean).
|
|
16119
|
+
* Fields placed here are merged into `[agents.profiles]` of Warp's global
|
|
16120
|
+
* `settings.toml`, while the shared `permission` block continues to drive the
|
|
16121
|
+
* `agent_mode_command_execution_allowlist`/`_denylist` command regex arrays.
|
|
16122
|
+
* Warp permissions are global-only.
|
|
16123
|
+
*
|
|
16124
|
+
* @example
|
|
16125
|
+
* { "agent_mode_coding_permissions": "always_allow_reading", "agent_mode_execute_readonly_commands": true }
|
|
16126
|
+
*/
|
|
16127
|
+
const WarpPermissionsOverrideSchema = z.looseObject({
|
|
16128
|
+
agent_mode_coding_permissions: z.optional(z.string()),
|
|
16129
|
+
agent_mode_coding_file_read_allowlist: z.optional(z.array(z.string())),
|
|
16130
|
+
agent_mode_execute_readonly_commands: z.optional(z.boolean())
|
|
16131
|
+
});
|
|
16132
|
+
/**
|
|
16133
|
+
* Tool-scoped override block for JetBrains Junie. Junie's `allowlist.json` has
|
|
16134
|
+
* two top-level autonomy knobs with no canonical per-glob slot:
|
|
16135
|
+
* `allowReadonlyCommands` (a boolean auto-allowing read-only commands) and
|
|
16136
|
+
* `defaultBehavior` (the fallback action applied when no rule matches — Junie
|
|
16137
|
+
* documents only `allow`/`ask`). Fields placed here are merged onto the
|
|
16138
|
+
* top level of `allowlist.json`, while the shared `permission` block continues
|
|
16139
|
+
* to drive the per-category `rules` groups.
|
|
16140
|
+
*
|
|
16141
|
+
* @example
|
|
16142
|
+
* { "allowReadonlyCommands": true, "defaultBehavior": "ask" }
|
|
16143
|
+
*/
|
|
16144
|
+
const JuniePermissionsOverrideSchema = z.looseObject({
|
|
16145
|
+
allowReadonlyCommands: z.optional(z.boolean()),
|
|
16146
|
+
defaultBehavior: z.optional(z.string())
|
|
16147
|
+
});
|
|
16148
|
+
/**
|
|
16149
|
+
* Tool-scoped override block for Takt. Takt's `config.yaml` carries two
|
|
16150
|
+
* permission surfaces the canonical coarse-mode mapping can't express:
|
|
16151
|
+
* `step_permission_overrides` — a per-workflow-step map (`<step>` →
|
|
16152
|
+
* `readonly`/`edit`/`full`) that lives inside the active provider profile
|
|
16153
|
+
* alongside `default_permission_mode` and layers on top of it at that step; and
|
|
16154
|
+
* `provider_options` — a top-level, per-provider table of sandbox/network knobs
|
|
16155
|
+
* orthogonal to the permission mode (e.g. `codex.network_access`,
|
|
16156
|
+
* `claude.sandbox.allow_unsandboxed_commands`, `opencode.allowed_tools`). Fields
|
|
16157
|
+
* placed here are merged into `config.yaml` and emitted only for Takt, while the
|
|
16158
|
+
* shared `permission` block continues to drive `default_permission_mode`. Kept
|
|
16159
|
+
* `looseObject` (verbatim passthrough); Takt validates its own value sets (e.g.
|
|
16160
|
+
* `provider_options.<p>.base_url` must be loopback). Both project and global
|
|
16161
|
+
* scope are supported.
|
|
16162
|
+
*
|
|
16163
|
+
* Note: Takt's config loader hard-rejects unknown top-level keys, so only keys
|
|
16164
|
+
* Takt actually recognizes belong here. `required_permission_mode` is NOT one —
|
|
16165
|
+
* it is a per-step field of the workflow YAML (not `config.yaml`), so it is out
|
|
16166
|
+
* of scope for this override.
|
|
16167
|
+
*
|
|
16168
|
+
* @example
|
|
16169
|
+
* { "step_permission_overrides": { "ai_review": "readonly" }, "provider_options": { "codex": { "network_access": true } } }
|
|
16170
|
+
*/
|
|
16171
|
+
const TaktPermissionsOverrideSchema = z.looseObject({
|
|
16172
|
+
step_permission_overrides: z.optional(z.record(z.string(), z.string())),
|
|
16173
|
+
provider_options: z.optional(z.looseObject({}))
|
|
16174
|
+
});
|
|
16175
|
+
/**
|
|
16176
|
+
* Tool-scoped override block for Amp. Amp's `amp.permissions` array and sibling
|
|
16177
|
+
* settings carry shapes the canonical per-command allow/ask/deny model can't
|
|
16178
|
+
* express, so they are authored here and merged into the shared Amp settings
|
|
16179
|
+
* file, while the shared `permission` block continues to drive the canonical
|
|
16180
|
+
* `amp.permissions` (allow/ask/reject) + `amp.tools.disable` entries:
|
|
16181
|
+
* - `permissions` — extra `amp.permissions` entries with non-`cmd` matchers
|
|
16182
|
+
* (`path`/`url`/`query`/…), regex/array match values, `context`
|
|
16183
|
+
* (`thread`/`subagent`), `delegate` (+`to`), or `reject` (+`message`). These
|
|
16184
|
+
* are appended AFTER the canonical-generated entries (Amp is first-match-wins,
|
|
16185
|
+
* so generated allow/ask/reject rules take precedence; authored entries act as
|
|
16186
|
+
* later fallbacks), preserving author order.
|
|
16187
|
+
* - `mcpPermissions` — Amp's `amp.mcpPermissions` array (`{ matches, action }`).
|
|
16188
|
+
* - `guardedFiles` — `amp.guardedFiles.allowlist` (globs allowed without
|
|
16189
|
+
* confirmation).
|
|
16190
|
+
* - `dangerouslyAllowAll` — `amp.dangerouslyAllowAll` (disable all confirmation).
|
|
16191
|
+
* Kept `looseObject` (verbatim passthrough). Both project and global scope are
|
|
16192
|
+
* supported.
|
|
16193
|
+
*
|
|
16194
|
+
* @example
|
|
16195
|
+
* { "dangerouslyAllowAll": false, "guardedFiles": { "allowlist": ["docs/**"] },
|
|
16196
|
+
* "permissions": [{ "tool": "Bash", "action": "delegate", "to": "approve.sh" }] }
|
|
16197
|
+
*/
|
|
16198
|
+
const AmpPermissionsOverrideSchema = z.looseObject({
|
|
16199
|
+
permissions: z.optional(z.array(z.looseObject({
|
|
16200
|
+
tool: z.string(),
|
|
16201
|
+
action: z.string()
|
|
16202
|
+
}))),
|
|
16203
|
+
mcpPermissions: z.optional(z.array(z.looseObject({}))),
|
|
16204
|
+
guardedFiles: z.optional(z.looseObject({ allowlist: z.optional(z.array(z.string())) })),
|
|
16205
|
+
dangerouslyAllowAll: z.optional(z.boolean())
|
|
16206
|
+
});
|
|
16207
|
+
/**
|
|
16208
|
+
* Tool-scoped override block for the Google Antigravity CLI. Antigravity's CLI
|
|
16209
|
+
* `settings.json` carries two global autonomy/sandbox knobs outside the
|
|
16210
|
+
* `permissions.allow/ask/deny` arrays rulesync manages: `toolPermission` (the
|
|
16211
|
+
* global autonomy preset — `request-review` (default) / `proceed-in-sandbox` /
|
|
16212
|
+
* `always-proceed` / `strict`) and `enableTerminalSandbox` (a boolean confining
|
|
16213
|
+
* agent-run commands to OS containment). Antigravity applies the allow/deny
|
|
16214
|
+
* lists as per-rule exceptions to the preset at runtime, so rulesync only
|
|
16215
|
+
* authors these keys verbatim — no precedence modeling is needed on our side.
|
|
16216
|
+
* Fields placed here are merged onto the top level of
|
|
16217
|
+
* `~/.gemini/antigravity-cli/settings.json` (global-only) and emitted only for
|
|
16218
|
+
* the CLI. The Antigravity IDE exposes the same concepts through a GUI (no
|
|
16219
|
+
* documented JSON schema), so this override does NOT apply to `antigravity-ide`.
|
|
16220
|
+
* Verified against https://antigravity.google/docs/cli/reference and
|
|
16221
|
+
* https://antigravity.google/docs/cli/sandbox.
|
|
16222
|
+
*
|
|
16223
|
+
* @example
|
|
16224
|
+
* { "toolPermission": "strict", "enableTerminalSandbox": true }
|
|
16225
|
+
*/
|
|
16226
|
+
const AntigravityCliPermissionsOverrideSchema = z.looseObject({
|
|
16227
|
+
toolPermission: z.optional(z.string()),
|
|
16228
|
+
enableTerminalSandbox: z.optional(z.boolean())
|
|
16229
|
+
});
|
|
16230
|
+
/**
|
|
15750
16231
|
* Permissions configuration.
|
|
15751
16232
|
* Keys are tool category names (e.g., "bash", "edit", "read", "webfetch").
|
|
15752
16233
|
* Values are pattern-to-action mappings for that tool category.
|
|
15753
16234
|
*
|
|
16235
|
+
* The optional `opencode`/`hermes`/`cline`/`kilo`/`claudecode`/`vibe`/`cursor`/
|
|
16236
|
+
* `qwencode`/`reasonix`/`factorydroid`/`warp`/`junie`/`takt`/`amp`/
|
|
16237
|
+
* `antigravity-cli` keys are tool-scoped overrides consumed only by their
|
|
16238
|
+
* respective translator (see the matching `*PermissionsOverrideSchema`); every
|
|
16239
|
+
* other tool reads the shared `permission` block and ignores them.
|
|
16240
|
+
*
|
|
15754
16241
|
* @example
|
|
15755
16242
|
* {
|
|
15756
16243
|
* "bash": { "*": "ask", "git *": "allow", "rm *": "deny" },
|
|
15757
16244
|
* "edit": { "*": "deny", "src/**": "allow" }
|
|
15758
16245
|
* }
|
|
15759
16246
|
*/
|
|
15760
|
-
const PermissionsConfigSchema = z.looseObject({
|
|
16247
|
+
const PermissionsConfigSchema = z.looseObject({
|
|
16248
|
+
permission: z.record(z.string(), PermissionRulesSchema),
|
|
16249
|
+
opencode: z.optional(OpencodePermissionsOverrideSchema),
|
|
16250
|
+
hermes: z.optional(HermesPermissionsOverrideSchema),
|
|
16251
|
+
cline: z.optional(ClinePermissionsOverrideSchema),
|
|
16252
|
+
kilo: z.optional(KiloPermissionsOverrideSchema),
|
|
16253
|
+
claudecode: z.optional(ClaudecodePermissionsOverrideSchema),
|
|
16254
|
+
vibe: z.optional(VibePermissionsOverrideSchema),
|
|
16255
|
+
cursor: z.optional(CursorPermissionsOverrideSchema),
|
|
16256
|
+
qwencode: z.optional(QwencodePermissionsOverrideSchema),
|
|
16257
|
+
reasonix: z.optional(ReasonixPermissionsOverrideSchema),
|
|
16258
|
+
factorydroid: z.optional(FactorydroidPermissionsOverrideSchema),
|
|
16259
|
+
warp: z.optional(WarpPermissionsOverrideSchema),
|
|
16260
|
+
junie: z.optional(JuniePermissionsOverrideSchema),
|
|
16261
|
+
takt: z.optional(TaktPermissionsOverrideSchema),
|
|
16262
|
+
amp: z.optional(AmpPermissionsOverrideSchema),
|
|
16263
|
+
"antigravity-cli": z.optional(AntigravityCliPermissionsOverrideSchema)
|
|
16264
|
+
});
|
|
15761
16265
|
/**
|
|
15762
16266
|
* Full permissions file schema including optional $schema field.
|
|
15763
16267
|
*/
|
|
@@ -15868,6 +16372,39 @@ const AMP_TOOLS_DISABLE_KEY = "amp.tools.disable";
|
|
|
15868
16372
|
* Reference: https://ampcode.com/manual ("amp.permissions").
|
|
15869
16373
|
*/
|
|
15870
16374
|
const AMP_PERMISSIONS_KEY = "amp.permissions";
|
|
16375
|
+
/**
|
|
16376
|
+
* The `amp.guardedFiles.allowlist` array (file globs allowed without
|
|
16377
|
+
* confirmation), `amp.dangerouslyAllowAll` boolean (disable all confirmation),
|
|
16378
|
+
* and `amp.mcpPermissions` array — sibling settings authored through the `amp`
|
|
16379
|
+
* permissions override. Reference: https://ampcode.com/manual.
|
|
16380
|
+
*/
|
|
16381
|
+
const AMP_GUARDED_FILES_ALLOWLIST_KEY = "amp.guardedFiles.allowlist";
|
|
16382
|
+
const AMP_DANGEROUSLY_ALLOW_ALL_KEY = "amp.dangerouslyAllowAll";
|
|
16383
|
+
const AMP_MCP_PERMISSIONS_KEY = "amp.mcpPermissions";
|
|
16384
|
+
/**
|
|
16385
|
+
* The read-only `cmd` string of an entry's `matches`, or `undefined` when the
|
|
16386
|
+
* matcher is absent or uses a non-`cmd` key / non-string value.
|
|
16387
|
+
*/
|
|
16388
|
+
function ampMatchesCmd(entry) {
|
|
16389
|
+
const cmd = entry.matches?.cmd;
|
|
16390
|
+
return typeof cmd === "string" ? cmd : void 0;
|
|
16391
|
+
}
|
|
16392
|
+
/**
|
|
16393
|
+
* Whether an `amp.permissions` entry is fully expressible in the canonical
|
|
16394
|
+
* per-command allow/ask/deny model: a plain `allow`/`ask`/`reject` action with
|
|
16395
|
+
* no `context`/`message`/`to` and a matcher that is either absent or exactly a
|
|
16396
|
+
* single string `cmd`. Everything else (non-`cmd` matchers, regex/array match
|
|
16397
|
+
* values, `delegate`, `reject`+`message`, `context`) is routed to the `amp`
|
|
16398
|
+
* override verbatim instead of being flattened with loss or dropped.
|
|
16399
|
+
*/
|
|
16400
|
+
function isCanonicalAmpEntry(entry) {
|
|
16401
|
+
if (entry.action === "delegate") return false;
|
|
16402
|
+
if (entry.context !== void 0 || entry.message !== void 0 || entry.to !== void 0) return false;
|
|
16403
|
+
const matches = entry.matches;
|
|
16404
|
+
if (matches === void 0) return true;
|
|
16405
|
+
const keys = Object.keys(matches);
|
|
16406
|
+
return keys.length === 0 || keys.length === 1 && typeof matches.cmd === "string";
|
|
16407
|
+
}
|
|
15871
16408
|
function parseAmpSettings(fileContent) {
|
|
15872
16409
|
const errors = [];
|
|
15873
16410
|
const parsed = parse(fileContent || "{}", errors, { allowTrailingComma: true });
|
|
@@ -15883,10 +16420,20 @@ function toDisableList(value) {
|
|
|
15883
16420
|
return value.filter((entry) => typeof entry === "string");
|
|
15884
16421
|
}
|
|
15885
16422
|
/**
|
|
16423
|
+
* Read `amp.guardedFiles.allowlist` (an array of file glob strings) from parsed
|
|
16424
|
+
* settings, returning `undefined` when the key is absent or not an array so the
|
|
16425
|
+
* override omits it.
|
|
16426
|
+
*/
|
|
16427
|
+
function extractAmpGuardedAllowlist(value) {
|
|
16428
|
+
if (!Array.isArray(value)) return void 0;
|
|
16429
|
+
return value.filter((entry) => typeof entry === "string");
|
|
16430
|
+
}
|
|
16431
|
+
/**
|
|
15886
16432
|
* Read an `amp.permissions` array from untrusted parsed settings. Only entries
|
|
15887
|
-
* whose
|
|
15888
|
-
*
|
|
15889
|
-
*
|
|
16433
|
+
* whose `tool`/`action` rulesync understands are retained; every other key —
|
|
16434
|
+
* including the full `matches` object (so non-`cmd` matchers, regex/array
|
|
16435
|
+
* values, `context`, `to`, `message`) — is kept verbatim so it survives a
|
|
16436
|
+
* round-trip.
|
|
15890
16437
|
*/
|
|
15891
16438
|
function toPermissionsList(value) {
|
|
15892
16439
|
if (!Array.isArray(value)) return [];
|
|
@@ -15896,14 +16443,11 @@ function toPermissionsList(value) {
|
|
|
15896
16443
|
const { tool, action } = raw;
|
|
15897
16444
|
if (typeof tool !== "string" || typeof action !== "string") continue;
|
|
15898
16445
|
if (action !== "allow" && action !== "reject" && action !== "ask" && action !== "delegate") continue;
|
|
15899
|
-
const matches = raw.matches;
|
|
15900
|
-
let normalizedMatches;
|
|
15901
|
-
if (isPlainObject(matches) && typeof matches.cmd === "string") normalizedMatches = { cmd: matches.cmd };
|
|
15902
16446
|
entries.push({
|
|
15903
16447
|
...raw,
|
|
15904
16448
|
tool,
|
|
15905
16449
|
action,
|
|
15906
|
-
...
|
|
16450
|
+
...isPlainObject(raw.matches) ? { matches: raw.matches } : {}
|
|
15907
16451
|
});
|
|
15908
16452
|
}
|
|
15909
16453
|
return entries;
|
|
@@ -15928,6 +16472,13 @@ function toPermissionsList(value) {
|
|
|
15928
16472
|
* The settings file is shared with the MCP feature (`amp.mcpServers`), so reads
|
|
15929
16473
|
* and writes merge into the existing JSON rather than overwriting it, and the
|
|
15930
16474
|
* file is never deleted.
|
|
16475
|
+
*
|
|
16476
|
+
* Amp shapes with no canonical category are authored and round-tripped through
|
|
16477
|
+
* the `amp` permissions override (see `AmpPermissionsOverrideSchema`): extra
|
|
16478
|
+
* `amp.permissions` entries with non-`cmd` matchers / `context` / `delegate` /
|
|
16479
|
+
* `reject`+`message` (appended after the generated canonical entries), plus the
|
|
16480
|
+
* sibling `amp.mcpPermissions`, `amp.guardedFiles.allowlist`, and
|
|
16481
|
+
* `amp.dangerouslyAllowAll` settings.
|
|
15931
16482
|
*/
|
|
15932
16483
|
var AmpPermissions = class AmpPermissions extends ToolPermissions {
|
|
15933
16484
|
constructor(params) {
|
|
@@ -15997,15 +16548,25 @@ var AmpPermissions = class AmpPermissions extends ToolPermissions {
|
|
|
15997
16548
|
const jsonDir = join(outputRoot, basePaths.relativeDirPath);
|
|
15998
16549
|
const { fileContent, relativeFilePath } = await this.resolveSettingsFile(jsonDir);
|
|
15999
16550
|
const json = fileContent ? parseAmpSettings(fileContent) : {};
|
|
16000
|
-
const
|
|
16001
|
-
const
|
|
16551
|
+
const config = rulesyncPermissions.getJson();
|
|
16552
|
+
const { disable, permissions } = convertRulesyncToAmp(config);
|
|
16553
|
+
const override = config.amp;
|
|
16554
|
+
const authoredExtras = override?.permissions ? toPermissionsList(override.permissions) : [];
|
|
16555
|
+
const preservedDelegates = override?.permissions ? [] : toPermissionsList(json[AMP_PERMISSIONS_KEY]).filter((entry) => entry.action === "delegate");
|
|
16002
16556
|
const newJson = {
|
|
16003
16557
|
...json,
|
|
16004
16558
|
[AMP_TOOLS_DISABLE_KEY]: disable
|
|
16005
16559
|
};
|
|
16006
|
-
const mergedPermissions = [
|
|
16560
|
+
const mergedPermissions = mergeAmpPermissions([
|
|
16561
|
+
...permissions,
|
|
16562
|
+
...authoredExtras,
|
|
16563
|
+
...preservedDelegates
|
|
16564
|
+
]);
|
|
16007
16565
|
if (mergedPermissions.length > 0) newJson[AMP_PERMISSIONS_KEY] = mergedPermissions;
|
|
16008
16566
|
else delete newJson[AMP_PERMISSIONS_KEY];
|
|
16567
|
+
if (override?.guardedFiles?.allowlist !== void 0) newJson[AMP_GUARDED_FILES_ALLOWLIST_KEY] = override.guardedFiles.allowlist;
|
|
16568
|
+
if (override?.dangerouslyAllowAll !== void 0) newJson[AMP_DANGEROUSLY_ALLOW_ALL_KEY] = override.dangerouslyAllowAll;
|
|
16569
|
+
if (override?.mcpPermissions !== void 0) newJson[AMP_MCP_PERMISSIONS_KEY] = override.mcpPermissions;
|
|
16009
16570
|
return new AmpPermissions({
|
|
16010
16571
|
outputRoot,
|
|
16011
16572
|
relativeDirPath: basePaths.relativeDirPath,
|
|
@@ -16016,11 +16577,22 @@ var AmpPermissions = class AmpPermissions extends ToolPermissions {
|
|
|
16016
16577
|
}
|
|
16017
16578
|
toRulesyncPermissions() {
|
|
16018
16579
|
const json = parseAmpSettings(this.getFileContent());
|
|
16580
|
+
const allPermissions = toPermissionsList(json[AMP_PERMISSIONS_KEY]);
|
|
16581
|
+
const canonicalEntries = allPermissions.filter(isCanonicalAmpEntry);
|
|
16582
|
+
const overrideEntries = allPermissions.filter((entry) => !isCanonicalAmpEntry(entry));
|
|
16019
16583
|
const config = convertAmpToRulesync({
|
|
16020
16584
|
disable: toDisableList(json[AMP_TOOLS_DISABLE_KEY]),
|
|
16021
|
-
permissions:
|
|
16022
|
-
});
|
|
16023
|
-
|
|
16585
|
+
permissions: canonicalEntries
|
|
16586
|
+
});
|
|
16587
|
+
const ampOverride = {};
|
|
16588
|
+
if (overrideEntries.length > 0) ampOverride.permissions = overrideEntries;
|
|
16589
|
+
if (Array.isArray(json[AMP_MCP_PERMISSIONS_KEY])) ampOverride.mcpPermissions = json[AMP_MCP_PERMISSIONS_KEY];
|
|
16590
|
+
const allowlist = extractAmpGuardedAllowlist(json[AMP_GUARDED_FILES_ALLOWLIST_KEY]);
|
|
16591
|
+
if (allowlist !== void 0) ampOverride.guardedFiles = { allowlist };
|
|
16592
|
+
if (typeof json[AMP_DANGEROUSLY_ALLOW_ALL_KEY] === "boolean") ampOverride.dangerouslyAllowAll = json[AMP_DANGEROUSLY_ALLOW_ALL_KEY];
|
|
16593
|
+
const result = { ...config };
|
|
16594
|
+
if (Object.keys(ampOverride).length > 0) result.amp = ampOverride;
|
|
16595
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
|
|
16024
16596
|
}
|
|
16025
16597
|
validate() {
|
|
16026
16598
|
try {
|
|
@@ -16067,6 +16639,37 @@ const ACTION_PRIORITY = {
|
|
|
16067
16639
|
allow: 2
|
|
16068
16640
|
};
|
|
16069
16641
|
/**
|
|
16642
|
+
* Fail-closed action priority for merging the full `amp.permissions` list
|
|
16643
|
+
* (generated + authored/preserved). `reject` leads so no `allow` can shadow it
|
|
16644
|
+
* under first-match-wins; `delegate` (which defers to an external approver, so
|
|
16645
|
+
* it never auto-allows) trails as the final fallback.
|
|
16646
|
+
*/
|
|
16647
|
+
const MERGE_ACTION_PRIORITY = {
|
|
16648
|
+
reject: 0,
|
|
16649
|
+
ask: 1,
|
|
16650
|
+
allow: 2,
|
|
16651
|
+
delegate: 3
|
|
16652
|
+
};
|
|
16653
|
+
/**
|
|
16654
|
+
* Stable fail-closed merge of generated and authored `amp.permissions` entries:
|
|
16655
|
+
* order by {@link MERGE_ACTION_PRIORITY} only, preserving each source's relative
|
|
16656
|
+
* order within an action class. Guarantees an authored `reject` cannot be
|
|
16657
|
+
* shadowed by a generated catch-all `allow`.
|
|
16658
|
+
*/
|
|
16659
|
+
function mergeAmpPermissions(entries) {
|
|
16660
|
+
const decorated = entries.map((entry, index) => ({
|
|
16661
|
+
entry,
|
|
16662
|
+
index
|
|
16663
|
+
}));
|
|
16664
|
+
decorated.sort((a, b) => {
|
|
16665
|
+
const ap = MERGE_ACTION_PRIORITY[a.entry.action] ?? MERGE_ACTION_PRIORITY.allow;
|
|
16666
|
+
const bp = MERGE_ACTION_PRIORITY[b.entry.action] ?? MERGE_ACTION_PRIORITY.allow;
|
|
16667
|
+
if (ap !== bp) return ap - bp;
|
|
16668
|
+
return a.index - b.index;
|
|
16669
|
+
});
|
|
16670
|
+
return decorated.map((d) => d.entry);
|
|
16671
|
+
}
|
|
16672
|
+
/**
|
|
16070
16673
|
* Convert a rulesync permissions config into Amp's two permission surfaces.
|
|
16071
16674
|
*
|
|
16072
16675
|
* For each `(category, pattern, action)` — where the category name IS the Amp
|
|
@@ -16131,14 +16734,14 @@ function sortAmpPermissions(entries) {
|
|
|
16131
16734
|
const ap = ACTION_PRIORITY[a.entry.action] ?? 0;
|
|
16132
16735
|
const bp = ACTION_PRIORITY[b.entry.action] ?? 0;
|
|
16133
16736
|
if (ap !== bp) return ap - bp;
|
|
16134
|
-
const aHasCmd = a.entry
|
|
16135
|
-
const bHasCmd = b.entry
|
|
16737
|
+
const aHasCmd = ampMatchesCmd(a.entry) !== void 0 ? 0 : 1;
|
|
16738
|
+
const bHasCmd = ampMatchesCmd(b.entry) !== void 0 ? 0 : 1;
|
|
16136
16739
|
if (aHasCmd !== bHasCmd) return aHasCmd - bHasCmd;
|
|
16137
16740
|
const at = a.entry.tool;
|
|
16138
16741
|
const bt = b.entry.tool;
|
|
16139
16742
|
if (at !== bt) return at < bt ? -1 : 1;
|
|
16140
|
-
const ac = a.entry
|
|
16141
|
-
const bc = b.entry
|
|
16743
|
+
const ac = ampMatchesCmd(a.entry) ?? "";
|
|
16744
|
+
const bc = ampMatchesCmd(b.entry) ?? "";
|
|
16142
16745
|
if (ac !== bc) return ac < bc ? -1 : 1;
|
|
16143
16746
|
return a.index - b.index;
|
|
16144
16747
|
});
|
|
@@ -16177,7 +16780,7 @@ function convertAmpToRulesync({ disable, permissions }) {
|
|
|
16177
16780
|
for (const tool of disable) assign(tool, "*", "deny");
|
|
16178
16781
|
for (const entry of permissions) {
|
|
16179
16782
|
if (entry.action === "delegate") continue;
|
|
16180
|
-
const pattern = entry
|
|
16783
|
+
const pattern = ampMatchesCmd(entry) ?? "*";
|
|
16181
16784
|
const action = entry.action === "reject" ? "deny" : entry.action === "ask" ? "ask" : "allow";
|
|
16182
16785
|
assign(entry.tool, pattern, action);
|
|
16183
16786
|
}
|
|
@@ -16259,6 +16862,11 @@ function buildPermissionEntry$1(toolName, pattern) {
|
|
|
16259
16862
|
* Permissions are written to the global `~/.gemini/antigravity-cli/settings.json`
|
|
16260
16863
|
* file (global scope only). The file holds other CLI settings besides
|
|
16261
16864
|
* permissions, so it is never deleted.
|
|
16865
|
+
*
|
|
16866
|
+
* Two CLI-only autonomy/sandbox knobs outside the allow/ask/deny arrays —
|
|
16867
|
+
* `toolPermission` (the global autonomy preset) and `enableTerminalSandbox` — are
|
|
16868
|
+
* authored and round-tripped through the `antigravity-cli` permissions override
|
|
16869
|
+
* (see `AntigravityCliPermissionsOverrideSchema`).
|
|
16262
16870
|
*/
|
|
16263
16871
|
var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPermissions {
|
|
16264
16872
|
constructor(params) {
|
|
@@ -16319,6 +16927,9 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
|
|
|
16319
16927
|
...settings,
|
|
16320
16928
|
permissions: mergedPermissions
|
|
16321
16929
|
};
|
|
16930
|
+
const override = config["antigravity-cli"];
|
|
16931
|
+
if (override?.toolPermission !== void 0) merged.toolPermission = override.toolPermission;
|
|
16932
|
+
if (override?.enableTerminalSandbox !== void 0) merged.enableTerminalSandbox = override.enableTerminalSandbox;
|
|
16322
16933
|
const fileContent = JSON.stringify(merged, null, 2);
|
|
16323
16934
|
return new AntigravityCliPermissions({
|
|
16324
16935
|
outputRoot,
|
|
@@ -16342,7 +16953,12 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
|
|
|
16342
16953
|
ask: permissions.ask ?? [],
|
|
16343
16954
|
deny: permissions.deny ?? []
|
|
16344
16955
|
});
|
|
16345
|
-
|
|
16956
|
+
const override = {};
|
|
16957
|
+
if (typeof settings.toolPermission === "string") override.toolPermission = settings.toolPermission;
|
|
16958
|
+
if (typeof settings.enableTerminalSandbox === "boolean") override.enableTerminalSandbox = settings.enableTerminalSandbox;
|
|
16959
|
+
const result = { ...config };
|
|
16960
|
+
if (Object.keys(override).length > 0) result["antigravity-cli"] = override;
|
|
16961
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
|
|
16346
16962
|
}
|
|
16347
16963
|
validate() {
|
|
16348
16964
|
return {
|
|
@@ -17128,6 +17744,14 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
|
|
|
17128
17744
|
}
|
|
17129
17745
|
const config = rulesyncPermissions.getJson();
|
|
17130
17746
|
const { allow, ask, deny } = convertRulesyncToClaudePermissions(config);
|
|
17747
|
+
const overridePermissions = config.claudecode?.permissions;
|
|
17748
|
+
if (overridePermissions && typeof overridePermissions === "object") {
|
|
17749
|
+
const { allow: _a, ask: _k, deny: _d, ...nonListFields } = overridePermissions;
|
|
17750
|
+
settings.permissions = {
|
|
17751
|
+
...settings.permissions,
|
|
17752
|
+
...nonListFields
|
|
17753
|
+
};
|
|
17754
|
+
}
|
|
17131
17755
|
const managedToolNames = new Set(Object.keys(config.permission).map((category) => toClaudeToolName(category)));
|
|
17132
17756
|
const merged = applyPermissions({
|
|
17133
17757
|
settings,
|
|
@@ -17160,6 +17784,8 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
|
|
|
17160
17784
|
ask: permissions.ask ?? [],
|
|
17161
17785
|
deny: permissions.deny ?? []
|
|
17162
17786
|
});
|
|
17787
|
+
const { allow: _a, ask: _k, deny: _d, ...nonListFields } = permissions;
|
|
17788
|
+
if (Object.keys(nonListFields).length > 0) config.claudecode = { permissions: nonListFields };
|
|
17163
17789
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
|
|
17164
17790
|
}
|
|
17165
17791
|
validate() {
|
|
@@ -17333,7 +17959,8 @@ var ClinePermissions = class ClinePermissions extends ToolPermissions {
|
|
|
17333
17959
|
} catch (error) {
|
|
17334
17960
|
throw new Error(`Failed to parse existing Cline command-permissions at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
17335
17961
|
}
|
|
17336
|
-
const
|
|
17962
|
+
const config = rulesyncPermissions.getJson();
|
|
17963
|
+
const { allow, deny, droppedCategories, translatedAskPatterns } = translateClinePermissions(config.permission);
|
|
17337
17964
|
warnClineTranslationNotices({
|
|
17338
17965
|
droppedCategories,
|
|
17339
17966
|
translatedAskPatterns,
|
|
@@ -17349,7 +17976,7 @@ var ClinePermissions = class ClinePermissions extends ToolPermissions {
|
|
|
17349
17976
|
...existing,
|
|
17350
17977
|
allow: dedupedAllow,
|
|
17351
17978
|
deny: mergedDeny,
|
|
17352
|
-
allowRedirects: existing.allowRedirects ?? false
|
|
17979
|
+
allowRedirects: config.cline?.allowRedirects ?? existing.allowRedirects ?? false
|
|
17353
17980
|
};
|
|
17354
17981
|
return new ClinePermissions({
|
|
17355
17982
|
outputRoot,
|
|
@@ -17373,6 +18000,7 @@ var ClinePermissions = class ClinePermissions extends ToolPermissions {
|
|
|
17373
18000
|
for (const pattern of parsed.allow ?? []) bashRules[pattern] = "allow";
|
|
17374
18001
|
for (const pattern of parsed.deny ?? []) bashRules[pattern] = "deny";
|
|
17375
18002
|
const config = Object.keys(bashRules).length > 0 ? { permission: { bash: bashRules } } : { permission: {} };
|
|
18003
|
+
if (parsed.allowRedirects === true) config.cline = { allowRedirects: true };
|
|
17376
18004
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
|
|
17377
18005
|
}
|
|
17378
18006
|
validate() {
|
|
@@ -17809,13 +18437,13 @@ const CURSOR_TYPE_TO_CANONICAL = {
|
|
|
17809
18437
|
WebFetch: "webfetch",
|
|
17810
18438
|
Mcp: "mcp"
|
|
17811
18439
|
};
|
|
17812
|
-
const MCP_CANONICAL_PREFIX = "mcp__";
|
|
18440
|
+
const MCP_CANONICAL_PREFIX$1 = "mcp__";
|
|
17813
18441
|
/**
|
|
17814
18442
|
* Returns true if the canonical category is the per-tool MCP form
|
|
17815
18443
|
* `mcp__<server>__<tool>`.
|
|
17816
18444
|
*/
|
|
17817
18445
|
function isMcpScopedCategory(canonical) {
|
|
17818
|
-
return canonical.startsWith(MCP_CANONICAL_PREFIX) && canonical.length > 5;
|
|
18446
|
+
return canonical.startsWith(MCP_CANONICAL_PREFIX$1) && canonical.length > 5;
|
|
17819
18447
|
}
|
|
17820
18448
|
function toCursorType(canonical) {
|
|
17821
18449
|
if (isMcpScopedCategory(canonical)) return "Mcp";
|
|
@@ -17846,7 +18474,7 @@ function toCursorPattern(canonical, pattern) {
|
|
|
17846
18474
|
function toCanonicalCategory$1(cursorType, pattern) {
|
|
17847
18475
|
if (cursorType === "Mcp") {
|
|
17848
18476
|
const match = pattern.match(/^([^:()]+):([^:()]+)$/);
|
|
17849
|
-
if (match) return `${MCP_CANONICAL_PREFIX}${match[1] ?? "*"}__${match[2] ?? "*"}`;
|
|
18477
|
+
if (match) return `${MCP_CANONICAL_PREFIX$1}${match[1] ?? "*"}__${match[2] ?? "*"}`;
|
|
17850
18478
|
return CURSOR_TYPE_TO_CANONICAL[cursorType] ?? cursorType.toLowerCase();
|
|
17851
18479
|
}
|
|
17852
18480
|
return CURSOR_TYPE_TO_CANONICAL[cursorType] ?? cursorType.toLowerCase();
|
|
@@ -17980,8 +18608,10 @@ var CursorPermissions = class CursorPermissions extends ToolPermissions {
|
|
|
17980
18608
|
mergedPermissions.allow = mergedAllow;
|
|
17981
18609
|
if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
|
|
17982
18610
|
else delete mergedPermissions.deny;
|
|
18611
|
+
const cursorOverride = config.cursor;
|
|
17983
18612
|
const merged = {
|
|
17984
18613
|
...settings,
|
|
18614
|
+
...cursorOverride,
|
|
17985
18615
|
version: settings.version ?? 1,
|
|
17986
18616
|
editor: {
|
|
17987
18617
|
...existingEditor,
|
|
@@ -18007,11 +18637,15 @@ var CursorPermissions = class CursorPermissions extends ToolPermissions {
|
|
|
18007
18637
|
}
|
|
18008
18638
|
const permissionsRaw = settings.permissions;
|
|
18009
18639
|
const permissions = permissionsRaw !== null && typeof permissionsRaw === "object" && !Array.isArray(permissionsRaw) ? permissionsRaw : {};
|
|
18010
|
-
const
|
|
18640
|
+
const result = { ...convertCursorToRulesyncPermissions({
|
|
18011
18641
|
allow: asCursorPermissionEntryArray(permissions.allow),
|
|
18012
18642
|
deny: asCursorPermissionEntryArray(permissions.deny)
|
|
18013
|
-
});
|
|
18014
|
-
|
|
18643
|
+
}) };
|
|
18644
|
+
const cursorOverride = {};
|
|
18645
|
+
if (settings.approvalMode !== void 0) cursorOverride.approvalMode = settings.approvalMode;
|
|
18646
|
+
if (settings.sandbox !== null && typeof settings.sandbox === "object" && !Array.isArray(settings.sandbox)) cursorOverride.sandbox = settings.sandbox;
|
|
18647
|
+
if (Object.keys(cursorOverride).length > 0) result.cursor = cursorOverride;
|
|
18648
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
|
|
18015
18649
|
}
|
|
18016
18650
|
validate() {
|
|
18017
18651
|
return {
|
|
@@ -18070,7 +18704,7 @@ function convertCursorToRulesyncPermissions(params) {
|
|
|
18070
18704
|
const { type, pattern } = parseCursorPermissionEntry(entry);
|
|
18071
18705
|
const canonical = toCanonicalCategory$1(type, pattern);
|
|
18072
18706
|
if (!permission[canonical]) permission[canonical] = {};
|
|
18073
|
-
const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX) ? "*" : pattern;
|
|
18707
|
+
const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$1) ? "*" : pattern;
|
|
18074
18708
|
permission[canonical][canonicalPattern] = action;
|
|
18075
18709
|
}
|
|
18076
18710
|
};
|
|
@@ -18319,6 +18953,16 @@ function convertDevinToRulesyncPermissions(params) {
|
|
|
18319
18953
|
}
|
|
18320
18954
|
//#endregion
|
|
18321
18955
|
//#region src/features/permissions/factorydroid-permissions.ts
|
|
18956
|
+
const FACTORYDROID_OVERRIDE_KEYS = [
|
|
18957
|
+
"commandBlocklist",
|
|
18958
|
+
"networkPolicy",
|
|
18959
|
+
"sandbox",
|
|
18960
|
+
"mcpPolicy",
|
|
18961
|
+
"enableDroidShield",
|
|
18962
|
+
"sessionDefaultSettings",
|
|
18963
|
+
"maxAutonomyLevel",
|
|
18964
|
+
"interactionMode"
|
|
18965
|
+
];
|
|
18322
18966
|
/**
|
|
18323
18967
|
* Permissions adapter for Factory Droid.
|
|
18324
18968
|
*
|
|
@@ -18336,18 +18980,14 @@ function convertDevinToRulesyncPermissions(params) {
|
|
|
18336
18980
|
* skipped (with a warning when they carry `deny` rules, to surface the gap).
|
|
18337
18981
|
*
|
|
18338
18982
|
* Factory Droid also has a stronger `commandBlocklist` tier — commands that can
|
|
18339
|
-
* never run, not even under full autonomy
|
|
18340
|
-
*
|
|
18341
|
-
*
|
|
18342
|
-
*
|
|
18343
|
-
*
|
|
18344
|
-
*
|
|
18345
|
-
*
|
|
18346
|
-
*
|
|
18347
|
-
* `commandBlocklist` and re-exporting it to a *fresh* config writes it back as
|
|
18348
|
-
* `commandDenylist`, not `commandBlocklist` — the hard-block tier is not
|
|
18349
|
-
* reconstructed. Re-running over the original file keeps it intact via the
|
|
18350
|
-
* verbatim preservation above.)
|
|
18983
|
+
* never run, not even under full autonomy — plus other security controls
|
|
18984
|
+
* (`networkPolicy`, `sandbox`, `mcpPolicy`, `enableDroidShield`, autonomy
|
|
18985
|
+
* settings) that do not fit the canonical `allow | ask | deny` per-command
|
|
18986
|
+
* model. These are authored and round-tripped through the `factorydroid`
|
|
18987
|
+
* override namespace (see `FactorydroidPermissionsOverrideSchema`): on **import**
|
|
18988
|
+
* they are lifted from `settings.json` into the override, and on **export** they
|
|
18989
|
+
* are merged back in — so `commandBlocklist`'s never-runs guarantee is preserved
|
|
18990
|
+
* faithfully rather than being collapsed onto an approvable `deny`.
|
|
18351
18991
|
*/
|
|
18352
18992
|
var FactorydroidPermissions = class FactorydroidPermissions extends ToolPermissions {
|
|
18353
18993
|
constructor(params) {
|
|
@@ -18390,11 +19030,16 @@ var FactorydroidPermissions = class FactorydroidPermissions extends ToolPermissi
|
|
|
18390
19030
|
} catch (error) {
|
|
18391
19031
|
throw new Error(`Failed to parse existing Factory Droid settings at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
18392
19032
|
}
|
|
19033
|
+
const config = rulesyncPermissions.getJson();
|
|
18393
19034
|
const { allow, deny } = convertRulesyncToFactorydroidPermissions({
|
|
18394
|
-
config
|
|
19035
|
+
config,
|
|
18395
19036
|
logger
|
|
18396
19037
|
});
|
|
18397
|
-
const
|
|
19038
|
+
const override = config.factorydroid;
|
|
19039
|
+
const merged = {
|
|
19040
|
+
...settings,
|
|
19041
|
+
...override !== void 0 && typeof override === "object" ? override : {}
|
|
19042
|
+
};
|
|
18398
19043
|
const mergedAllow = uniq(allow.toSorted());
|
|
18399
19044
|
const mergedDeny = uniq(deny.toSorted());
|
|
18400
19045
|
if (mergedAllow.length > 0) merged.commandAllowlist = mergedAllow;
|
|
@@ -18418,10 +19063,13 @@ var FactorydroidPermissions = class FactorydroidPermissions extends ToolPermissi
|
|
|
18418
19063
|
}
|
|
18419
19064
|
const config = convertFactorydroidToRulesyncPermissions({
|
|
18420
19065
|
allow: Array.isArray(settings.commandAllowlist) ? settings.commandAllowlist : [],
|
|
18421
|
-
deny: Array.isArray(settings.commandDenylist) ? settings.commandDenylist : []
|
|
18422
|
-
block: Array.isArray(settings.commandBlocklist) ? settings.commandBlocklist : []
|
|
19066
|
+
deny: Array.isArray(settings.commandDenylist) ? settings.commandDenylist : []
|
|
18423
19067
|
});
|
|
18424
|
-
|
|
19068
|
+
const factorydroidOverride = {};
|
|
19069
|
+
for (const key of FACTORYDROID_OVERRIDE_KEYS) if (settings[key] !== void 0) factorydroidOverride[key] = settings[key];
|
|
19070
|
+
const result = { ...config };
|
|
19071
|
+
if (Object.keys(factorydroidOverride).length > 0) result.factorydroid = factorydroidOverride;
|
|
19072
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
|
|
18425
19073
|
}
|
|
18426
19074
|
validate() {
|
|
18427
19075
|
return {
|
|
@@ -18468,18 +19116,18 @@ function convertRulesyncToFactorydroidPermissions({ config, logger }) {
|
|
|
18468
19116
|
};
|
|
18469
19117
|
}
|
|
18470
19118
|
/**
|
|
18471
|
-
* Convert Factory Droid allow/deny
|
|
18472
|
-
*
|
|
19119
|
+
* Convert Factory Droid allow/deny command lists back to rulesync config under
|
|
19120
|
+
* the `bash` category.
|
|
18473
19121
|
*
|
|
18474
|
-
* `commandBlocklist` (hard
|
|
18475
|
-
*
|
|
18476
|
-
* the
|
|
19122
|
+
* `commandBlocklist` (the hard-block tier) is no longer collapsed here — it has
|
|
19123
|
+
* no canonical equivalent and now round-trips through the `factorydroid`
|
|
19124
|
+
* override so the never-runs guarantee is preserved instead of being weakened to
|
|
19125
|
+
* an approvable `deny`.
|
|
18477
19126
|
*/
|
|
18478
|
-
function convertFactorydroidToRulesyncPermissions({ allow, deny
|
|
19127
|
+
function convertFactorydroidToRulesyncPermissions({ allow, deny }) {
|
|
18479
19128
|
const bash = {};
|
|
18480
19129
|
for (const pattern of allow) bash[pattern] = "allow";
|
|
18481
19130
|
for (const pattern of deny) bash[pattern] = "deny";
|
|
18482
|
-
for (const pattern of block) bash[pattern] = "deny";
|
|
18483
19131
|
return { permission: Object.keys(bash).length > 0 ? { bash } : {} };
|
|
18484
19132
|
}
|
|
18485
19133
|
//#endregion
|
|
@@ -18673,30 +19321,114 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
|
|
|
18673
19321
|
const GROKCLI_GLOBAL_ONLY_MESSAGE = "Grok CLI permissions are global-only; use --global to sync ~/.grok/config.toml";
|
|
18674
19322
|
const GROKCLI_UI_KEY = "ui";
|
|
18675
19323
|
const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
|
|
19324
|
+
const GROKCLI_PERMISSION_KEY = "permission";
|
|
18676
19325
|
const CATCH_ALL_PATTERN$2 = "*";
|
|
19326
|
+
const MCP_CANONICAL_PREFIX = "mcp__";
|
|
19327
|
+
const CATEGORY_TO_GROK_TOOL = {
|
|
19328
|
+
bash: "Bash",
|
|
19329
|
+
read: "Read",
|
|
19330
|
+
edit: "Edit",
|
|
19331
|
+
write: "Edit",
|
|
19332
|
+
grep: "Grep",
|
|
19333
|
+
webfetch: "WebFetch"
|
|
19334
|
+
};
|
|
19335
|
+
const GROK_TOOL_TO_CATEGORY = {
|
|
19336
|
+
Bash: "bash",
|
|
19337
|
+
Read: "read",
|
|
19338
|
+
Edit: "edit",
|
|
19339
|
+
Grep: "grep",
|
|
19340
|
+
WebFetch: "webfetch"
|
|
19341
|
+
};
|
|
19342
|
+
const GROK_MCP_TOOL = "MCPTool";
|
|
19343
|
+
/**
|
|
19344
|
+
* Build a Grok Claude-style permission entry (e.g. `Bash(git *)`, `Read`,
|
|
19345
|
+
* `MCPTool(server__tool)`) from a canonical category + pattern. Returns `null`
|
|
19346
|
+
* for categories Grok cannot express so callers can skip them.
|
|
19347
|
+
*
|
|
19348
|
+
* Scoped MCP categories (`mcp__<remainder>`) fold their address into the
|
|
19349
|
+
* parentheses (`MCPTool(<remainder>)`); the bare `mcp` category becomes
|
|
19350
|
+
* `MCPTool`. For every other tool, a `*` pattern emits the bare tool name and a
|
|
19351
|
+
* concrete pattern emits `Tool(pattern)`.
|
|
19352
|
+
*/
|
|
19353
|
+
function buildGrokEntry(category, pattern) {
|
|
19354
|
+
if (category.startsWith(MCP_CANONICAL_PREFIX)) {
|
|
19355
|
+
const remainder = category.slice(5);
|
|
19356
|
+
return remainder.length > 0 ? `${GROK_MCP_TOOL}(${remainder})` : GROK_MCP_TOOL;
|
|
19357
|
+
}
|
|
19358
|
+
if (category === "mcp") return pattern === CATCH_ALL_PATTERN$2 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
|
|
19359
|
+
const tool = CATEGORY_TO_GROK_TOOL[category];
|
|
19360
|
+
if (tool === void 0) return null;
|
|
19361
|
+
return pattern === CATCH_ALL_PATTERN$2 || pattern === "" ? tool : `${tool}(${pattern})`;
|
|
19362
|
+
}
|
|
19363
|
+
/**
|
|
19364
|
+
* Parse a Grok Claude-style permission entry back into a canonical category +
|
|
19365
|
+
* pattern. Entries without parentheses are treated as catch-all (`*`). Returns
|
|
19366
|
+
* `null` for tool prefixes with no canonical equivalent (e.g. `any`).
|
|
19367
|
+
*/
|
|
19368
|
+
function parseGrokEntry(entry) {
|
|
19369
|
+
const trimmed = entry.trim();
|
|
19370
|
+
const parenIndex = trimmed.indexOf("(");
|
|
19371
|
+
let tool;
|
|
19372
|
+
let inner;
|
|
19373
|
+
if (parenIndex === -1 || !trimmed.endsWith(")")) {
|
|
19374
|
+
tool = trimmed;
|
|
19375
|
+
inner = "";
|
|
19376
|
+
} else {
|
|
19377
|
+
tool = trimmed.slice(0, parenIndex);
|
|
19378
|
+
inner = trimmed.slice(parenIndex + 1, -1).trim();
|
|
19379
|
+
}
|
|
19380
|
+
if (tool === GROK_MCP_TOOL) return inner.length > 0 ? {
|
|
19381
|
+
category: `${MCP_CANONICAL_PREFIX}${inner}`,
|
|
19382
|
+
pattern: CATCH_ALL_PATTERN$2
|
|
19383
|
+
} : {
|
|
19384
|
+
category: "mcp",
|
|
19385
|
+
pattern: CATCH_ALL_PATTERN$2
|
|
19386
|
+
};
|
|
19387
|
+
const category = GROK_TOOL_TO_CATEGORY[tool];
|
|
19388
|
+
if (category === void 0) return null;
|
|
19389
|
+
return {
|
|
19390
|
+
category,
|
|
19391
|
+
pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$2
|
|
19392
|
+
};
|
|
19393
|
+
}
|
|
18677
19394
|
/**
|
|
18678
19395
|
* Permissions adapter for the xAI Grok Build CLI (`grokcli`).
|
|
18679
19396
|
*
|
|
18680
|
-
* Grok
|
|
18681
|
-
*
|
|
18682
|
-
*
|
|
18683
|
-
*
|
|
18684
|
-
*
|
|
18685
|
-
*
|
|
18686
|
-
*
|
|
18687
|
-
*
|
|
18688
|
-
*
|
|
18689
|
-
*
|
|
18690
|
-
* `
|
|
18691
|
-
*
|
|
18692
|
-
*
|
|
18693
|
-
*
|
|
18694
|
-
* the
|
|
18695
|
-
*
|
|
18696
|
-
*
|
|
18697
|
-
*
|
|
18698
|
-
*
|
|
18699
|
-
*
|
|
19397
|
+
* Grok Build CLI ships a Claude-style rule system under `[permission]` in
|
|
19398
|
+
* `~/.grok/config.toml`: `allow` / `deny` / `ask` arrays of entries such as
|
|
19399
|
+
* `Bash(git *)`, `Read(src/**)`, `Edit`, `Grep`, `MCPTool(server__tool)`, and
|
|
19400
|
+
* `WebFetch`, evaluated with precedence `deny > ask > allow`
|
|
19401
|
+
* (https://docs.x.ai/build/settings/reference). rulesync's canonical
|
|
19402
|
+
* per-category, per-pattern model maps almost 1:1:
|
|
19403
|
+
* - Generate: each `permission.<category>.<pattern> = allow|ask|deny` becomes
|
|
19404
|
+
* the matching Grok entry and is bucketed into the `[permission]` array for
|
|
19405
|
+
* that action. `bash|read|edit|grep|webfetch` map to their Grok tool;
|
|
19406
|
+
* `write` collapses onto `Edit` (Grok has no `Write` tool); `mcp__*` maps to
|
|
19407
|
+
* `MCPTool(...)` (a scoped MCP category folds its address into the
|
|
19408
|
+
* parentheses, so a non-`*` argument pattern on it is not represented).
|
|
19409
|
+
* Categories with no Grok tool (`websearch`, `glob`, `notebookedit`,
|
|
19410
|
+
* `agent`) are skipped (with a warning when they carry a `deny` rule, to
|
|
19411
|
+
* surface the gap). When two canonical rules collapse onto the same Grok
|
|
19412
|
+
* entry with different actions (e.g. `edit` allow + `write` deny → `Edit`),
|
|
19413
|
+
* the strictest wins (`deny > ask > allow`) and a warning is logged, so the
|
|
19414
|
+
* entry never lands contradictorily in two arrays.
|
|
19415
|
+
* - Import: the `[permission]` arrays are parsed back into canonical
|
|
19416
|
+
* categories. When no `[permission]` section is present (older configs), the
|
|
19417
|
+
* coarse `[ui] permission_mode` is used as a fallback.
|
|
19418
|
+
*
|
|
19419
|
+
* The coarse `[ui] permission_mode` toggle is still written for backward
|
|
19420
|
+
* compatibility with older Grok versions: `always-approve` when the config is
|
|
19421
|
+
* pure-`allow`, otherwise `ask` (conservative — it is never `always-approve`
|
|
19422
|
+
* while any `deny`/`ask` rule exists, so it never contradicts the fine-grained
|
|
19423
|
+
* arrays).
|
|
19424
|
+
*
|
|
19425
|
+
* This surface is **global only** — the adapter syncs the user-level
|
|
19426
|
+
* `~/.grok/config.toml`. (Grok also supports a project-scoped `[permission]`
|
|
19427
|
+
* file; project scope is not modeled here.) The shared config is merged in
|
|
19428
|
+
* place: rulesync owns the `[permission]` `allow`/`deny`/`ask` entries for the
|
|
19429
|
+
* tools it models and the `[ui] permission_mode` value, while every other key
|
|
19430
|
+
* (e.g. `[mcp_servers]`, `[permission] rules`, `[sandbox]`) and any user-authored
|
|
19431
|
+
* entries for tools rulesync cannot model (e.g. `WebSearch`) are preserved. The
|
|
18700
19432
|
* file is never deleted.
|
|
18701
19433
|
*/
|
|
18702
19434
|
var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
@@ -18728,7 +19460,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
|
18728
19460
|
global: true
|
|
18729
19461
|
});
|
|
18730
19462
|
}
|
|
18731
|
-
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false }) {
|
|
19463
|
+
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
|
|
18732
19464
|
if (!global) throw new Error(GROKCLI_GLOBAL_ONLY_MESSAGE);
|
|
18733
19465
|
const paths = GrokcliPermissions.getSettablePaths({ global });
|
|
18734
19466
|
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
@@ -18739,7 +19471,16 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
|
18739
19471
|
} catch (error) {
|
|
18740
19472
|
throw new Error(`Failed to parse existing Grok config.toml at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
18741
19473
|
}
|
|
18742
|
-
const
|
|
19474
|
+
const config = rulesyncPermissions.getJson();
|
|
19475
|
+
const existingPermission = isRecord(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {};
|
|
19476
|
+
const buckets = buildGrokPermissionArrays(config, existingPermission, logger);
|
|
19477
|
+
parsed[GROKCLI_PERMISSION_KEY] = {
|
|
19478
|
+
...existingPermission,
|
|
19479
|
+
allow: buckets.allow,
|
|
19480
|
+
deny: buckets.deny,
|
|
19481
|
+
ask: buckets.ask
|
|
19482
|
+
};
|
|
19483
|
+
const mode = deriveGrokPermissionMode(config);
|
|
18743
19484
|
const existingUi = isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {};
|
|
18744
19485
|
parsed[GROKCLI_UI_KEY] = {
|
|
18745
19486
|
...existingUi,
|
|
@@ -18762,8 +19503,8 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
|
18762
19503
|
} catch (error) {
|
|
18763
19504
|
throw new Error(`Failed to parse Grok config.toml content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
18764
19505
|
}
|
|
18765
|
-
const
|
|
18766
|
-
const rulesyncConfig = { permission: { bash: { [CATCH_ALL_PATTERN$2]:
|
|
19506
|
+
const fineGrained = parseGrokPermissionArrays(isRecord(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {});
|
|
19507
|
+
const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$2]: legacyModeAction(parsed) } } };
|
|
18767
19508
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
|
|
18768
19509
|
}
|
|
18769
19510
|
validate() {
|
|
@@ -18783,6 +19524,84 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
|
18783
19524
|
});
|
|
18784
19525
|
}
|
|
18785
19526
|
};
|
|
19527
|
+
const ACTION_RANK = {
|
|
19528
|
+
allow: 0,
|
|
19529
|
+
ask: 1,
|
|
19530
|
+
deny: 2
|
|
19531
|
+
};
|
|
19532
|
+
/**
|
|
19533
|
+
* Collect user-authored entries from an existing `[permission]` array whose
|
|
19534
|
+
* tool prefix rulesync cannot model (e.g. `WebSearch`, `any`). Such entries are
|
|
19535
|
+
* preserved verbatim so replacing the arrays does not silently drop them
|
|
19536
|
+
* (mirrors the Cursor adapter's preservation of unmanaged types).
|
|
19537
|
+
*/
|
|
19538
|
+
function unmanagedEntries(existingPermission, key) {
|
|
19539
|
+
return (isStringArray(existingPermission[key]) ? existingPermission[key] : []).filter((entry) => parseGrokEntry(entry) === null);
|
|
19540
|
+
}
|
|
19541
|
+
/**
|
|
19542
|
+
* Bucket canonical rules into Grok's `[permission]` allow/deny/ask arrays of
|
|
19543
|
+
* Claude-style entries, resolving collapse collisions via `deny > ask > allow`.
|
|
19544
|
+
* Categories Grok cannot express are skipped (with a warning when they carry a
|
|
19545
|
+
* `deny` rule); entries the user authored for tools rulesync cannot model are
|
|
19546
|
+
* preserved from `existingPermission`.
|
|
19547
|
+
*/
|
|
19548
|
+
function buildGrokPermissionArrays(config, existingPermission, logger) {
|
|
19549
|
+
const ranked = /* @__PURE__ */ new Map();
|
|
19550
|
+
for (const [category, rules] of Object.entries(config.permission)) for (const [pattern, action] of Object.entries(rules)) {
|
|
19551
|
+
const entry = buildGrokEntry(category, pattern);
|
|
19552
|
+
if (entry === null) {
|
|
19553
|
+
if (action === "deny" && logger) logger.warn(`Grok CLI has no permission tool for the '${category}' category; its 'deny' rule could not be represented and was skipped.`);
|
|
19554
|
+
continue;
|
|
19555
|
+
}
|
|
19556
|
+
const existing = ranked.get(entry);
|
|
19557
|
+
if (existing !== void 0 && existing !== action) logger?.warn(`Grok permission entry '${entry}' received conflicting actions ('${existing}' and '${action}'); keeping the stricter one (deny > ask > allow).`);
|
|
19558
|
+
if (existing === void 0 || ACTION_RANK[action] > ACTION_RANK[existing]) ranked.set(entry, action);
|
|
19559
|
+
}
|
|
19560
|
+
const allow = unmanagedEntries(existingPermission, "allow");
|
|
19561
|
+
const deny = unmanagedEntries(existingPermission, "deny");
|
|
19562
|
+
const ask = unmanagedEntries(existingPermission, "ask");
|
|
19563
|
+
for (const [entry, action] of ranked) if (action === "allow") allow.push(entry);
|
|
19564
|
+
else if (action === "deny") deny.push(entry);
|
|
19565
|
+
else ask.push(entry);
|
|
19566
|
+
return {
|
|
19567
|
+
allow: uniq(allow.toSorted()),
|
|
19568
|
+
deny: uniq(deny.toSorted()),
|
|
19569
|
+
ask: uniq(ask.toSorted())
|
|
19570
|
+
};
|
|
19571
|
+
}
|
|
19572
|
+
/**
|
|
19573
|
+
* Parse Grok's `[permission]` allow/deny/ask arrays back into a canonical
|
|
19574
|
+
* permission map. Returns `null` when the section defines none of the three
|
|
19575
|
+
* arrays, so the caller can fall back to the coarse `permission_mode`.
|
|
19576
|
+
* Precedence `deny > ask > allow` is applied so a tool listed in multiple
|
|
19577
|
+
* arrays resolves to the strictest action.
|
|
19578
|
+
*/
|
|
19579
|
+
function parseGrokPermissionArrays(permission) {
|
|
19580
|
+
const allow = isStringArray(permission.allow) ? permission.allow : void 0;
|
|
19581
|
+
const deny = isStringArray(permission.deny) ? permission.deny : void 0;
|
|
19582
|
+
const ask = isStringArray(permission.ask) ? permission.ask : void 0;
|
|
19583
|
+
if ((allow?.length ?? 0) + (deny?.length ?? 0) + (ask?.length ?? 0) === 0) return null;
|
|
19584
|
+
const result = {};
|
|
19585
|
+
const apply = (entries, action) => {
|
|
19586
|
+
for (const entry of entries ?? []) {
|
|
19587
|
+
const parsed = parseGrokEntry(entry);
|
|
19588
|
+
if (parsed === null) continue;
|
|
19589
|
+
const bucket = result[parsed.category] ??= {};
|
|
19590
|
+
bucket[parsed.pattern] = action;
|
|
19591
|
+
}
|
|
19592
|
+
};
|
|
19593
|
+
apply(allow, "allow");
|
|
19594
|
+
apply(ask, "ask");
|
|
19595
|
+
apply(deny, "deny");
|
|
19596
|
+
return result;
|
|
19597
|
+
}
|
|
19598
|
+
/**
|
|
19599
|
+
* Legacy coarse fallback: map `[ui] permission_mode` to a canonical action.
|
|
19600
|
+
* `always-approve` ⇒ `allow`; anything else (including a missing mode) ⇒ `ask`.
|
|
19601
|
+
*/
|
|
19602
|
+
function legacyModeAction(parsed) {
|
|
19603
|
+
return (isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {})[GROKCLI_PERMISSION_MODE_KEY] === "always-approve" ? "allow" : "ask";
|
|
19604
|
+
}
|
|
18786
19605
|
/**
|
|
18787
19606
|
* Collapse a rulesync permissions config into Grok's single coarse mode.
|
|
18788
19607
|
* Any `deny`/`ask` rule anywhere keeps prompting (`ask`); otherwise an existing
|
|
@@ -18798,6 +19617,10 @@ function deriveGrokPermissionMode(config) {
|
|
|
18798
19617
|
}
|
|
18799
19618
|
//#endregion
|
|
18800
19619
|
//#region src/features/permissions/hermesagent-permissions.ts
|
|
19620
|
+
/** Collect the glob patterns in a canonical category that carry a given action. */
|
|
19621
|
+
function patternsByAction(category, action) {
|
|
19622
|
+
return Object.entries(category ?? {}).filter(([, value]) => value === action).map(([pattern]) => pattern);
|
|
19623
|
+
}
|
|
18801
19624
|
var HermesagentPermissions = class HermesagentPermissions extends ToolPermissions {
|
|
18802
19625
|
static getSettablePaths() {
|
|
18803
19626
|
return {
|
|
@@ -18821,10 +19644,21 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
|
|
|
18821
19644
|
return true;
|
|
18822
19645
|
}
|
|
18823
19646
|
setFileContent(fileContent) {
|
|
18824
|
-
this.fileContent =
|
|
19647
|
+
this.fileContent = applySharedConfigPatch({
|
|
19648
|
+
fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
|
|
19649
|
+
feature: "permissions",
|
|
19650
|
+
existingContent: fileContent,
|
|
19651
|
+
patch: parseSharedConfig({
|
|
19652
|
+
format: "yaml",
|
|
19653
|
+
fileContent: this.fileContent
|
|
19654
|
+
})
|
|
19655
|
+
});
|
|
18825
19656
|
}
|
|
18826
19657
|
toRulesyncPermissions() {
|
|
18827
|
-
const config =
|
|
19658
|
+
const config = parseSharedConfig({
|
|
19659
|
+
format: "yaml",
|
|
19660
|
+
fileContent: this.getFileContent()
|
|
19661
|
+
});
|
|
18828
19662
|
const permissions = config.permissions && typeof config.permissions === "object" ? config.permissions.rulesync : {};
|
|
18829
19663
|
return new RulesyncPermissions({
|
|
18830
19664
|
relativeDirPath: "",
|
|
@@ -18834,11 +19668,27 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
|
|
|
18834
19668
|
}
|
|
18835
19669
|
static fromRulesyncPermissions({ outputRoot, rulesyncPermissions }) {
|
|
18836
19670
|
const permissions = rulesyncPermissions.getJson();
|
|
19671
|
+
const permissionBlock = permissions.permission ?? {};
|
|
19672
|
+
const commandAllowlist = Object.entries(permissionBlock).flatMap(([, patterns]) => patternsByAction(patterns, "allow"));
|
|
19673
|
+
const bashDeny = patternsByAction(permissionBlock.bash, "deny");
|
|
19674
|
+
const webfetchDeny = patternsByAction(permissionBlock.webfetch, "deny");
|
|
19675
|
+
let config = {};
|
|
19676
|
+
if (commandAllowlist.length > 0) config.command_allowlist = commandAllowlist;
|
|
19677
|
+
if (bashDeny.length > 0) config.approvals = { deny: bashDeny };
|
|
19678
|
+
if (webfetchDeny.length > 0) config.security = { website_blocklist: {
|
|
19679
|
+
enabled: true,
|
|
19680
|
+
domains: webfetchDeny
|
|
19681
|
+
} };
|
|
19682
|
+
if (permissions.hermes && typeof permissions.hermes === "object") config = mergeSharedConfigDeep({
|
|
19683
|
+
base: config,
|
|
19684
|
+
patch: permissions.hermes
|
|
19685
|
+
});
|
|
19686
|
+
config.permissions = { rulesync: permissions };
|
|
18837
19687
|
return new HermesagentPermissions({
|
|
18838
19688
|
outputRoot,
|
|
18839
|
-
fileContent:
|
|
18840
|
-
|
|
18841
|
-
|
|
19689
|
+
fileContent: stringifySharedConfig({
|
|
19690
|
+
format: "yaml",
|
|
19691
|
+
document: config
|
|
18842
19692
|
})
|
|
18843
19693
|
});
|
|
18844
19694
|
}
|
|
@@ -18860,14 +19710,18 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
|
|
|
18860
19710
|
* "executables": [ { "prefix": "git ", "action": "allow" } ],
|
|
18861
19711
|
* "fileEditing": [ { "pattern": "src/**", "action": "allow" } ],
|
|
18862
19712
|
* "mcpTools": [ { "prefix": "search", "action": "allow" } ],
|
|
18863
|
-
* "readOutsideProject":[ { "pattern": "/etc/**", "action": "
|
|
19713
|
+
* "readOutsideProject":[ { "pattern": "/etc/**", "action": "ask" } ]
|
|
18864
19714
|
* }
|
|
18865
19715
|
* }
|
|
18866
19716
|
* ```
|
|
18867
19717
|
*
|
|
18868
19718
|
* Each rule carries a literal `prefix` (matches commands that start with it) or
|
|
18869
|
-
* a glob `pattern` (`*`, `**`, `?`, `[abc]`, `[!abc]`) plus an `action
|
|
18870
|
-
*
|
|
19719
|
+
* a glob `pattern` (`*`, `**`, `?`, `[abc]`, `[!abc]`) plus an `action`. Junie
|
|
19720
|
+
* documents only `allow` and `ask` as valid actions — there is **no `deny`**
|
|
19721
|
+
* (https://junie.jetbrains.com/docs/action-allowlist-junie-cli.html). rulesync's
|
|
19722
|
+
* canonical `allow`/`ask` map 1:1; a canonical `deny` has no Junie equivalent
|
|
19723
|
+
* and is mapped to the nearest valid action, `ask` (still withholds
|
|
19724
|
+
* auto-approval), with a warning so the downgrade is surfaced.
|
|
18871
19725
|
*
|
|
18872
19726
|
* Category mapping (rulesync canonical <-> Junie rule group):
|
|
18873
19727
|
* - `bash` <-> `executables`
|
|
@@ -18877,8 +19731,11 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
|
|
|
18877
19731
|
*
|
|
18878
19732
|
* Categories Junie cannot represent (e.g. `webfetch`) are skipped on export
|
|
18879
19733
|
* (with a warning when they carry rules). The top-level `defaultBehavior` and
|
|
18880
|
-
* `allowReadonlyCommands` settings have no canonical
|
|
18881
|
-
*
|
|
19734
|
+
* `allowReadonlyCommands` settings have no canonical per-glob slot: they are
|
|
19735
|
+
* authored and round-tripped through the `junie` override namespace (see
|
|
19736
|
+
* `JuniePermissionsOverrideSchema`) — lifted into the override on import and
|
|
19737
|
+
* merged back onto the top level on export — and any other unmodeled top-level
|
|
19738
|
+
* key is preserved verbatim.
|
|
18882
19739
|
*
|
|
18883
19740
|
* @see https://junie.jetbrains.com/docs/action-allowlist-junie-cli.html
|
|
18884
19741
|
*/
|
|
@@ -18901,7 +19758,6 @@ const JUNIE_GROUP_TO_CANONICAL = {
|
|
|
18901
19758
|
mcpTools: "mcp",
|
|
18902
19759
|
readOutsideProject: "read"
|
|
18903
19760
|
};
|
|
18904
|
-
const JUNIE_DEFAULT_BEHAVIOR = "ask";
|
|
18905
19761
|
function isPermissionAction$1(value) {
|
|
18906
19762
|
return PermissionActionSchema.safeParse(value).success;
|
|
18907
19763
|
}
|
|
@@ -18951,13 +19807,16 @@ var JuniePermissions = class JuniePermissions extends ToolPermissions {
|
|
|
18951
19807
|
} catch (error) {
|
|
18952
19808
|
throw new Error(`Failed to parse existing Junie allowlist at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
18953
19809
|
}
|
|
19810
|
+
const config = rulesyncPermissions.getJson();
|
|
18954
19811
|
const rules = convertRulesyncToJunieRules({
|
|
18955
|
-
config
|
|
19812
|
+
config,
|
|
18956
19813
|
logger
|
|
18957
19814
|
});
|
|
19815
|
+
const override = config.junie;
|
|
19816
|
+
const overrideObj = override !== void 0 && typeof override === "object" ? override : {};
|
|
18958
19817
|
const merged = {
|
|
18959
19818
|
...existing,
|
|
18960
|
-
|
|
19819
|
+
...overrideObj,
|
|
18961
19820
|
rules
|
|
18962
19821
|
};
|
|
18963
19822
|
return new JuniePermissions({
|
|
@@ -18977,7 +19836,12 @@ var JuniePermissions = class JuniePermissions extends ToolPermissions {
|
|
|
18977
19836
|
throw new Error(`Failed to parse Junie permissions content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
18978
19837
|
}
|
|
18979
19838
|
const config = convertJunieToRulesyncPermissions({ allowlist });
|
|
18980
|
-
|
|
19839
|
+
const junieOverride = {};
|
|
19840
|
+
if (typeof allowlist.allowReadonlyCommands === "boolean") junieOverride.allowReadonlyCommands = allowlist.allowReadonlyCommands;
|
|
19841
|
+
if (typeof allowlist.defaultBehavior === "string") junieOverride.defaultBehavior = allowlist.defaultBehavior;
|
|
19842
|
+
const result = { ...config };
|
|
19843
|
+
if (Object.keys(junieOverride).length > 0) result.junie = junieOverride;
|
|
19844
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
|
|
18981
19845
|
}
|
|
18982
19846
|
validate() {
|
|
18983
19847
|
return {
|
|
@@ -19009,12 +19873,13 @@ function convertRulesyncToJunieRules({ config, logger }) {
|
|
|
19009
19873
|
continue;
|
|
19010
19874
|
}
|
|
19011
19875
|
for (const [pattern, action] of Object.entries(patterns)) {
|
|
19876
|
+
const junieAction = toJunieAction(action, category, pattern, logger);
|
|
19012
19877
|
const rule = isGlobPattern(pattern) ? {
|
|
19013
19878
|
pattern,
|
|
19014
|
-
action
|
|
19879
|
+
action: junieAction
|
|
19015
19880
|
} : {
|
|
19016
19881
|
prefix: pattern,
|
|
19017
|
-
action
|
|
19882
|
+
action: junieAction
|
|
19018
19883
|
};
|
|
19019
19884
|
(rules[group] ??= []).push(rule);
|
|
19020
19885
|
}
|
|
@@ -19022,6 +19887,19 @@ function convertRulesyncToJunieRules({ config, logger }) {
|
|
|
19022
19887
|
return rules;
|
|
19023
19888
|
}
|
|
19024
19889
|
/**
|
|
19890
|
+
* Map a canonical action onto a valid Junie allowlist action. Junie supports
|
|
19891
|
+
* only `allow` and `ask`; a canonical `deny` is downgraded to `ask` (the
|
|
19892
|
+
* nearest valid action — both withhold auto-approval) with a warning, so
|
|
19893
|
+
* rulesync never emits a `deny` that Junie would silently ignore.
|
|
19894
|
+
*/
|
|
19895
|
+
function toJunieAction(action, category, pattern, logger) {
|
|
19896
|
+
if (action === "deny") {
|
|
19897
|
+
logger?.warn(`Junie's allowlist supports only 'allow'/'ask' actions; the '${category}' deny rule for '${pattern}' was downgraded to 'ask' (Junie has no 'deny').`);
|
|
19898
|
+
return "ask";
|
|
19899
|
+
}
|
|
19900
|
+
return action;
|
|
19901
|
+
}
|
|
19902
|
+
/**
|
|
19025
19903
|
* Convert a Junie allowlist back into rulesync permissions config. The
|
|
19026
19904
|
* top-level `defaultBehavior` / `allowReadonlyCommands` settings have no
|
|
19027
19905
|
* canonical equivalent and are not imported.
|
|
@@ -19055,6 +19933,31 @@ const KiloPermissionSchema = z.union([z.enum([
|
|
|
19055
19933
|
]))]);
|
|
19056
19934
|
const KiloPermissionsConfigSchema = z.looseObject({ permission: z.optional(z.record(z.string(), KiloPermissionSchema)) });
|
|
19057
19935
|
/**
|
|
19936
|
+
* Kilo permission keys that share a name with a canonical rulesync category and
|
|
19937
|
+
* therefore stay in the shared `permission` block. Everything else (Kilo-only
|
|
19938
|
+
* keys such as `external_directory`, `doom_loop`, `notebook_edit`, ...) is
|
|
19939
|
+
* routed into the `kilo` override on import so it does not leak into other
|
|
19940
|
+
* tools' configs. Kilo folds `write` into `edit`, uses `notebook_edit`/`task`
|
|
19941
|
+
* rather than the canonical `notebookedit`/`agent`, and has no `mcp` key (MCP is
|
|
19942
|
+
* addressed via `mcp__*` tool-name keys), so those canonical names are not Kilo
|
|
19943
|
+
* keys in practice — they simply never appear on the shared side.
|
|
19944
|
+
*/
|
|
19945
|
+
const KILO_SHARED_CATEGORIES = /* @__PURE__ */ new Set([
|
|
19946
|
+
"bash",
|
|
19947
|
+
"read",
|
|
19948
|
+
"edit",
|
|
19949
|
+
"write",
|
|
19950
|
+
"webfetch",
|
|
19951
|
+
"websearch",
|
|
19952
|
+
"grep",
|
|
19953
|
+
"glob",
|
|
19954
|
+
"notebookedit",
|
|
19955
|
+
"agent"
|
|
19956
|
+
]);
|
|
19957
|
+
function isSharedKiloCategory(key) {
|
|
19958
|
+
return key === "*" || KILO_SHARED_CATEGORIES.has(key) || key.startsWith("mcp__");
|
|
19959
|
+
}
|
|
19960
|
+
/**
|
|
19058
19961
|
* Parse a JSONC string and throw on syntax errors. The `jsonc-parser` `parse()` function is
|
|
19059
19962
|
* non-throwing best-effort: invalid input silently yields a partial value (often `undefined`,
|
|
19060
19963
|
* coerced to `{}` by callers). That behavior would silently drop a user's existing `deny` rules
|
|
@@ -19166,12 +20069,16 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
|
|
|
19166
20069
|
const parsed = parseKiloJsoncStrict(await readFileContentOrNull(filePath) ?? "{}", filePath);
|
|
19167
20070
|
const parsedPermission = parsed.permission;
|
|
19168
20071
|
const existingPermission = parsedPermission && typeof parsedPermission === "object" && !Array.isArray(parsedPermission) ? { ...parsedPermission } : {};
|
|
19169
|
-
const
|
|
20072
|
+
const rulesyncJson = rulesyncPermissions.getJson();
|
|
20073
|
+
const kiloOverride = rulesyncJson.kilo;
|
|
20074
|
+
const incomingPermission = {
|
|
20075
|
+
...rulesyncJson.permission,
|
|
20076
|
+
...kiloOverride?.permission
|
|
20077
|
+
};
|
|
19170
20078
|
const droppedDenyByKey = {};
|
|
19171
|
-
for (const key of Object.
|
|
19172
|
-
const
|
|
19173
|
-
const
|
|
19174
|
-
const nextDenyPatterns = new Set(collectKiloDenyPatterns(rulesyncPermission[key]));
|
|
20079
|
+
for (const [key, value] of Object.entries(incomingPermission)) {
|
|
20080
|
+
const previousDenyPatterns = collectKiloDenyPatterns(existingPermission[key]);
|
|
20081
|
+
const nextDenyPatterns = new Set(collectKiloDenyPatterns(value));
|
|
19175
20082
|
const dropped = previousDenyPatterns.filter((p) => !nextDenyPatterns.has(p));
|
|
19176
20083
|
if (dropped.length > 0) droppedDenyByKey[key] = dropped;
|
|
19177
20084
|
}
|
|
@@ -19179,8 +20086,10 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
|
|
|
19179
20086
|
const summary = Object.entries(droppedDenyByKey).map(([key, patterns]) => `${key}: [${patterns.join(", ")}]`).join("; ");
|
|
19180
20087
|
logger?.warn(`WARNING: Kilo permissions regeneration drops existing 'deny' rule(s) because rulesync output owns these tool keys. Dropped — ${summary}. To preserve these denies, add them to '.rulesync/permissions.json'.`);
|
|
19181
20088
|
}
|
|
19182
|
-
const mergedPermission = {
|
|
19183
|
-
|
|
20089
|
+
const mergedPermission = {
|
|
20090
|
+
...existingPermission,
|
|
20091
|
+
...incomingPermission
|
|
20092
|
+
};
|
|
19184
20093
|
const nextJson = {
|
|
19185
20094
|
...parsed,
|
|
19186
20095
|
permission: mergedPermission
|
|
@@ -19194,8 +20103,16 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
|
|
|
19194
20103
|
});
|
|
19195
20104
|
}
|
|
19196
20105
|
toRulesyncPermissions() {
|
|
19197
|
-
const
|
|
19198
|
-
|
|
20106
|
+
const rawPermission = this.json.permission ?? {};
|
|
20107
|
+
const shared = {};
|
|
20108
|
+
const overrideOnly = {};
|
|
20109
|
+
for (const [key, value] of Object.entries(rawPermission)) if (isSharedKiloCategory(key)) shared[key] = typeof value === "string" ? { "*": value } : value;
|
|
20110
|
+
else overrideOnly[key] = value;
|
|
20111
|
+
const json = Object.keys(overrideOnly).length > 0 ? {
|
|
20112
|
+
permission: shared,
|
|
20113
|
+
kilo: { permission: overrideOnly }
|
|
20114
|
+
} : { permission: shared };
|
|
20115
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(json, null, 2) });
|
|
19199
20116
|
}
|
|
19200
20117
|
validate() {
|
|
19201
20118
|
try {
|
|
@@ -19225,10 +20142,6 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
|
|
|
19225
20142
|
validate: false
|
|
19226
20143
|
});
|
|
19227
20144
|
}
|
|
19228
|
-
normalizePermission(permission) {
|
|
19229
|
-
if (!permission) return {};
|
|
19230
|
-
return Object.fromEntries(Object.entries(permission).map(([tool, value]) => [tool, typeof value === "string" ? { "*": value } : value]));
|
|
19231
|
-
}
|
|
19232
20145
|
};
|
|
19233
20146
|
//#endregion
|
|
19234
20147
|
//#region src/features/permissions/kiro-permissions.ts
|
|
@@ -19286,29 +20199,16 @@ var KiroPermissions = class KiroPermissions extends ToolPermissions {
|
|
|
19286
20199
|
}
|
|
19287
20200
|
const permission = {};
|
|
19288
20201
|
const toolsSettings = parsed.toolsSettings ?? {};
|
|
19289
|
-
const
|
|
19290
|
-
|
|
19291
|
-
const
|
|
19292
|
-
|
|
19293
|
-
|
|
19294
|
-
|
|
19295
|
-
|
|
19296
|
-
|
|
19297
|
-
|
|
19298
|
-
|
|
19299
|
-
const readDeny = asStringArray(readSettings.deniedPaths);
|
|
19300
|
-
if (readAllow.length > 0 || readDeny.length > 0) {
|
|
19301
|
-
permission.read = {};
|
|
19302
|
-
for (const pattern of readAllow) permission.read[pattern] = "allow";
|
|
19303
|
-
for (const pattern of readDeny) permission.read[pattern] = "deny";
|
|
19304
|
-
}
|
|
19305
|
-
const writeSettings = asRecord$1(toolsSettings.write);
|
|
19306
|
-
const writeAllow = asStringArray(writeSettings.allowedPaths);
|
|
19307
|
-
const writeDeny = asStringArray(writeSettings.deniedPaths);
|
|
19308
|
-
if (writeAllow.length > 0 || writeDeny.length > 0) {
|
|
19309
|
-
permission.write = {};
|
|
19310
|
-
for (const pattern of writeAllow) permission.write[pattern] = "allow";
|
|
19311
|
-
for (const pattern of writeDeny) permission.write[pattern] = "deny";
|
|
20202
|
+
const shellRules = rulesFromArrays(asRecord$1(toolsSettings.shell), "allowedCommands", "deniedCommands");
|
|
20203
|
+
if (Object.keys(shellRules).length > 0) permission.bash = shellRules;
|
|
20204
|
+
for (const category of [
|
|
20205
|
+
"read",
|
|
20206
|
+
"write",
|
|
20207
|
+
"grep",
|
|
20208
|
+
"glob"
|
|
20209
|
+
]) {
|
|
20210
|
+
const rules = rulesFromArrays(asRecord$1(toolsSettings[category]), "allowedPaths", "deniedPaths");
|
|
20211
|
+
if (Object.keys(rules).length > 0) permission[category] = rules;
|
|
19312
20212
|
}
|
|
19313
20213
|
const allowedTools = new Set(parsed.allowedTools ?? []);
|
|
19314
20214
|
if (allowedTools.has("web_fetch")) permission.webfetch = { "*": "allow" };
|
|
@@ -19334,45 +20234,63 @@ var KiroPermissions = class KiroPermissions extends ToolPermissions {
|
|
|
19334
20234
|
function buildKiroPermissionsFromRulesync({ config, logger, existing }) {
|
|
19335
20235
|
const nextAllowedTools = new Set(existing.allowedTools ?? []);
|
|
19336
20236
|
const nextToolsSettings = { ...asRecord$1(existing.toolsSettings) };
|
|
20237
|
+
const pathBuckets = {};
|
|
20238
|
+
const pushPath = (key, action, pattern) => {
|
|
20239
|
+
const bucket = pathBuckets[key] ??= {
|
|
20240
|
+
allow: [],
|
|
20241
|
+
deny: []
|
|
20242
|
+
};
|
|
20243
|
+
(action === "allow" ? bucket.allow : bucket.deny).push(pattern);
|
|
20244
|
+
};
|
|
19337
20245
|
const shell = {
|
|
19338
20246
|
allowedCommands: [],
|
|
19339
20247
|
deniedCommands: []
|
|
19340
20248
|
};
|
|
19341
|
-
const read = {
|
|
19342
|
-
allowedPaths: [],
|
|
19343
|
-
deniedPaths: []
|
|
19344
|
-
};
|
|
19345
|
-
const write = {
|
|
19346
|
-
allowedPaths: [],
|
|
19347
|
-
deniedPaths: []
|
|
19348
|
-
};
|
|
19349
20249
|
for (const [category, rules] of Object.entries(config.permission)) for (const [pattern, action] of Object.entries(rules)) {
|
|
19350
20250
|
if (action === "ask") {
|
|
19351
20251
|
logger?.warn(`Kiro permissions do not support "ask". Skipping ${category}:${pattern}`);
|
|
19352
20252
|
continue;
|
|
19353
20253
|
}
|
|
19354
20254
|
if (category === "bash") (action === "allow" ? shell.allowedCommands : shell.deniedCommands).push(pattern);
|
|
19355
|
-
else if (category === "read"
|
|
19356
|
-
else if (category === "edit" || category === "write") (
|
|
19357
|
-
else if (category === "webfetch" || category === "websearch") {
|
|
19358
|
-
|
|
19359
|
-
|
|
19360
|
-
|
|
19361
|
-
|
|
19362
|
-
|
|
19363
|
-
|
|
19364
|
-
|
|
19365
|
-
} else logger?.warn(`Kiro permissions do not support category: ${category}. Skipping.`);
|
|
20255
|
+
else if (category === "read" || category === "grep" || category === "glob") pushPath(category, action, pattern);
|
|
20256
|
+
else if (category === "edit" || category === "write") pushPath("write", action, pattern);
|
|
20257
|
+
else if (category === "webfetch" || category === "websearch") applyKiroWebPermission({
|
|
20258
|
+
category,
|
|
20259
|
+
pattern,
|
|
20260
|
+
action,
|
|
20261
|
+
nextAllowedTools,
|
|
20262
|
+
logger
|
|
20263
|
+
});
|
|
20264
|
+
else logger?.warn(`Kiro permissions do not support category: ${category}. Skipping.`);
|
|
19366
20265
|
}
|
|
19367
20266
|
nextToolsSettings.shell = shell;
|
|
19368
|
-
nextToolsSettings.read = read;
|
|
19369
|
-
nextToolsSettings.write = write;
|
|
20267
|
+
nextToolsSettings.read = pathTable(pathBuckets.read);
|
|
20268
|
+
nextToolsSettings.write = pathTable(pathBuckets.write);
|
|
20269
|
+
for (const key of ["grep", "glob"]) {
|
|
20270
|
+
const bucket = pathBuckets[key];
|
|
20271
|
+
if (bucket && (bucket.allow.length > 0 || bucket.deny.length > 0)) nextToolsSettings[key] = pathTable(bucket);
|
|
20272
|
+
}
|
|
19370
20273
|
return {
|
|
19371
20274
|
...existing,
|
|
19372
20275
|
allowedTools: [...nextAllowedTools].toSorted(),
|
|
19373
20276
|
toolsSettings: nextToolsSettings
|
|
19374
20277
|
};
|
|
19375
20278
|
}
|
|
20279
|
+
function pathTable(bucket) {
|
|
20280
|
+
return {
|
|
20281
|
+
allowedPaths: bucket?.allow ?? [],
|
|
20282
|
+
deniedPaths: bucket?.deny ?? []
|
|
20283
|
+
};
|
|
20284
|
+
}
|
|
20285
|
+
function applyKiroWebPermission({ category, pattern, action, nextAllowedTools, logger }) {
|
|
20286
|
+
if (pattern !== "*") {
|
|
20287
|
+
logger?.warn(`Kiro ${category} supports only wildcard (*) via allowedTools. Skipping rule: ${pattern}`);
|
|
20288
|
+
return;
|
|
20289
|
+
}
|
|
20290
|
+
const toolName = category === "webfetch" ? "web_fetch" : "web_search";
|
|
20291
|
+
if (action === "allow") nextAllowedTools.add(toolName);
|
|
20292
|
+
else nextAllowedTools.delete(toolName);
|
|
20293
|
+
}
|
|
19376
20294
|
function asRecord$1(value) {
|
|
19377
20295
|
const result = UnknownRecordSchema.safeParse(value);
|
|
19378
20296
|
return result.success ? result.data : {};
|
|
@@ -19380,6 +20298,17 @@ function asRecord$1(value) {
|
|
|
19380
20298
|
function asStringArray(value) {
|
|
19381
20299
|
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
19382
20300
|
}
|
|
20301
|
+
/**
|
|
20302
|
+
* Build a canonical `{ pattern: action }` map from a Kiro tool settings record's
|
|
20303
|
+
* allow/deny string arrays (e.g. `allowedPaths`/`deniedPaths` or
|
|
20304
|
+
* `allowedCommands`/`deniedCommands`).
|
|
20305
|
+
*/
|
|
20306
|
+
function rulesFromArrays(settings, allowKey, denyKey) {
|
|
20307
|
+
const rules = {};
|
|
20308
|
+
for (const pattern of asStringArray(settings[allowKey])) rules[pattern] = "allow";
|
|
20309
|
+
for (const pattern of asStringArray(settings[denyKey])) rules[pattern] = "deny";
|
|
20310
|
+
return rules;
|
|
20311
|
+
}
|
|
19383
20312
|
//#endregion
|
|
19384
20313
|
//#region src/features/permissions/opencode-permissions.ts
|
|
19385
20314
|
const OpencodePermissionSchema = z.union([z.enum([
|
|
@@ -19391,6 +20320,28 @@ const OpencodePermissionSchema = z.union([z.enum([
|
|
|
19391
20320
|
"ask",
|
|
19392
20321
|
"deny"
|
|
19393
20322
|
]))]);
|
|
20323
|
+
/**
|
|
20324
|
+
* Canonical rulesync permission categories that carry a cross-tool meaning (see
|
|
20325
|
+
* the "Supported tool categories" list in `docs/reference/file-formats.md`).
|
|
20326
|
+
* On import, any OpenCode category outside this set — plus MCP tool names — is
|
|
20327
|
+
* treated as OpenCode-only and routed into the `opencode` override block so a
|
|
20328
|
+
* subsequent `rulesync generate` does not leak it into other tools' configs.
|
|
20329
|
+
*/
|
|
20330
|
+
const CANONICAL_PERMISSION_CATEGORIES = /* @__PURE__ */ new Set([
|
|
20331
|
+
"bash",
|
|
20332
|
+
"read",
|
|
20333
|
+
"edit",
|
|
20334
|
+
"write",
|
|
20335
|
+
"webfetch",
|
|
20336
|
+
"websearch",
|
|
20337
|
+
"grep",
|
|
20338
|
+
"glob",
|
|
20339
|
+
"notebookedit",
|
|
20340
|
+
"agent"
|
|
20341
|
+
]);
|
|
20342
|
+
function isSharedPermissionCategory(category) {
|
|
20343
|
+
return category === "*" || CANONICAL_PERMISSION_CATEGORIES.has(category) || category.startsWith("mcp__");
|
|
20344
|
+
}
|
|
19394
20345
|
const OpencodePermissionsConfigSchema = z.looseObject({ permission: z.optional(z.union([z.enum([
|
|
19395
20346
|
"allow",
|
|
19396
20347
|
"ask",
|
|
@@ -19452,9 +20403,15 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
|
|
|
19452
20403
|
fileContent = await readFileContentOrNull(jsonPath);
|
|
19453
20404
|
if (fileContent) relativeFilePath = OPENCODE_JSON_FILE_NAME;
|
|
19454
20405
|
}
|
|
20406
|
+
const parsed = parse(fileContent ?? "{}");
|
|
20407
|
+
const rulesyncJson = rulesyncPermissions.getJson();
|
|
20408
|
+
const overridePermission = rulesyncJson.opencode?.permission ?? {};
|
|
19455
20409
|
const nextJson = {
|
|
19456
|
-
...
|
|
19457
|
-
permission:
|
|
20410
|
+
...parsed,
|
|
20411
|
+
permission: {
|
|
20412
|
+
...rulesyncJson.permission,
|
|
20413
|
+
...overridePermission
|
|
20414
|
+
}
|
|
19458
20415
|
};
|
|
19459
20416
|
return new OpencodePermissions({
|
|
19460
20417
|
outputRoot,
|
|
@@ -19465,8 +20422,20 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
|
|
|
19465
20422
|
});
|
|
19466
20423
|
}
|
|
19467
20424
|
toRulesyncPermissions() {
|
|
19468
|
-
const
|
|
19469
|
-
|
|
20425
|
+
const rawPermission = this.json.permission;
|
|
20426
|
+
if (rawPermission === void 0 || typeof rawPermission === "string") {
|
|
20427
|
+
const permission = this.normalizePermission(rawPermission);
|
|
20428
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
|
|
20429
|
+
}
|
|
20430
|
+
const shared = {};
|
|
20431
|
+
const overrideOnly = {};
|
|
20432
|
+
for (const [category, value] of Object.entries(rawPermission)) if (isSharedPermissionCategory(category)) shared[category] = typeof value === "string" ? { "*": value } : value;
|
|
20433
|
+
else overrideOnly[category] = value;
|
|
20434
|
+
const json = Object.keys(overrideOnly).length > 0 ? {
|
|
20435
|
+
permission: shared,
|
|
20436
|
+
opencode: { permission: overrideOnly }
|
|
20437
|
+
} : { permission: shared };
|
|
20438
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(json, null, 2) });
|
|
19470
20439
|
}
|
|
19471
20440
|
validate() {
|
|
19472
20441
|
try {
|
|
@@ -19496,10 +20465,15 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
|
|
|
19496
20465
|
validate: false
|
|
19497
20466
|
});
|
|
19498
20467
|
}
|
|
20468
|
+
/**
|
|
20469
|
+
* Normalize the uniform/undefined forms of OpenCode's `permission` field into
|
|
20470
|
+
* the canonical rulesync shape. The object form is handled directly in
|
|
20471
|
+
* `toRulesyncPermissions` (it needs to split shared vs OpenCode-only
|
|
20472
|
+
* categories), so this only covers the two remaining cases.
|
|
20473
|
+
*/
|
|
19499
20474
|
normalizePermission(permission) {
|
|
19500
20475
|
if (!permission) return {};
|
|
19501
|
-
|
|
19502
|
-
return Object.fromEntries(Object.entries(permission).map(([tool, value]) => [tool, typeof value === "string" ? { "*": value } : value]));
|
|
20476
|
+
return { "*": { "*": permission } };
|
|
19503
20477
|
}
|
|
19504
20478
|
};
|
|
19505
20479
|
//#endregion
|
|
@@ -19573,6 +20547,24 @@ function buildQwenPermissionEntry(toolName, pattern) {
|
|
|
19573
20547
|
if (pattern === "*") return toolName;
|
|
19574
20548
|
return `${toolName}(${pattern})`;
|
|
19575
20549
|
}
|
|
20550
|
+
const QWEN_OVERRIDE_TOOLS_KEYS = [
|
|
20551
|
+
"approvalMode",
|
|
20552
|
+
"autoAccept",
|
|
20553
|
+
"sandbox",
|
|
20554
|
+
"sandboxImage",
|
|
20555
|
+
"disabled"
|
|
20556
|
+
];
|
|
20557
|
+
const QWEN_OVERRIDE_SECURITY_KEYS = ["folderTrust"];
|
|
20558
|
+
function asPlainRecord(value) {
|
|
20559
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
20560
|
+
}
|
|
20561
|
+
/** Pick the override-managed keys out of a settings group into a fresh record. */
|
|
20562
|
+
function pickQwenOverrideKeys(group, keys) {
|
|
20563
|
+
const source = asPlainRecord(group);
|
|
20564
|
+
const picked = {};
|
|
20565
|
+
for (const key of keys) if (source[key] !== void 0) picked[key] = source[key];
|
|
20566
|
+
return picked;
|
|
20567
|
+
}
|
|
19576
20568
|
var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
|
|
19577
20569
|
constructor(params) {
|
|
19578
20570
|
super({
|
|
@@ -19638,6 +20630,15 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
|
|
|
19638
20630
|
...settings,
|
|
19639
20631
|
permissions: mergedPermissions
|
|
19640
20632
|
};
|
|
20633
|
+
const override = config.qwencode;
|
|
20634
|
+
if (override?.tools !== void 0) merged.tools = {
|
|
20635
|
+
...asPlainRecord(settings.tools),
|
|
20636
|
+
...asPlainRecord(override.tools)
|
|
20637
|
+
};
|
|
20638
|
+
if (override?.security !== void 0) merged.security = {
|
|
20639
|
+
...asPlainRecord(settings.security),
|
|
20640
|
+
...asPlainRecord(override.security)
|
|
20641
|
+
};
|
|
19641
20642
|
const fileContent = JSON.stringify(merged, null, 2);
|
|
19642
20643
|
return new QwencodePermissions({
|
|
19643
20644
|
outputRoot,
|
|
@@ -19663,7 +20664,14 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
|
|
|
19663
20664
|
ask: permissions.ask ?? [],
|
|
19664
20665
|
deny: permissions.deny ?? []
|
|
19665
20666
|
});
|
|
19666
|
-
|
|
20667
|
+
const overrideTools = pickQwenOverrideKeys(settings.tools, QWEN_OVERRIDE_TOOLS_KEYS);
|
|
20668
|
+
const overrideSecurity = pickQwenOverrideKeys(settings.security, QWEN_OVERRIDE_SECURITY_KEYS);
|
|
20669
|
+
const qwencodeOverride = {};
|
|
20670
|
+
if (Object.keys(overrideTools).length > 0) qwencodeOverride.tools = overrideTools;
|
|
20671
|
+
if (Object.keys(overrideSecurity).length > 0) qwencodeOverride.security = overrideSecurity;
|
|
20672
|
+
const result = { ...config };
|
|
20673
|
+
if (Object.keys(qwencodeOverride).length > 0) result.qwencode = qwencodeOverride;
|
|
20674
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
|
|
19667
20675
|
}
|
|
19668
20676
|
validate() {
|
|
19669
20677
|
try {
|
|
@@ -19824,6 +20832,16 @@ function toPermissionsTable(value) {
|
|
|
19824
20832
|
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
19825
20833
|
return { ...value };
|
|
19826
20834
|
}
|
|
20835
|
+
const REASONIX_OVERRIDE_AGENT_KEYS = ["plan_mode_allowed_tools", "plan_mode_read_only_commands"];
|
|
20836
|
+
function asReasonixRecord(value) {
|
|
20837
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
20838
|
+
}
|
|
20839
|
+
function pickReasonixKeys(source, keys) {
|
|
20840
|
+
const record = asReasonixRecord(source);
|
|
20841
|
+
const picked = {};
|
|
20842
|
+
for (const key of keys) if (record[key] !== void 0) picked[key] = record[key];
|
|
20843
|
+
return picked;
|
|
20844
|
+
}
|
|
19827
20845
|
var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
|
|
19828
20846
|
toml;
|
|
19829
20847
|
constructor(params) {
|
|
@@ -19885,6 +20903,15 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
|
|
|
19885
20903
|
...parsed,
|
|
19886
20904
|
permissions: mergedPermissions
|
|
19887
20905
|
};
|
|
20906
|
+
const override = config.reasonix;
|
|
20907
|
+
if (override?.sandbox !== void 0) merged.sandbox = {
|
|
20908
|
+
...asReasonixRecord(parsed.sandbox),
|
|
20909
|
+
...asReasonixRecord(override.sandbox)
|
|
20910
|
+
};
|
|
20911
|
+
if (override?.agent !== void 0) merged.agent = {
|
|
20912
|
+
...asReasonixRecord(parsed.agent),
|
|
20913
|
+
...asReasonixRecord(override.agent)
|
|
20914
|
+
};
|
|
19888
20915
|
const fileContent = smolToml.stringify(merged);
|
|
19889
20916
|
return new ReasonixPermissions({
|
|
19890
20917
|
outputRoot,
|
|
@@ -19901,7 +20928,14 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
|
|
|
19901
20928
|
ask: toStringArray$1(permissions.ask),
|
|
19902
20929
|
deny: toStringArray$1(permissions.deny)
|
|
19903
20930
|
});
|
|
19904
|
-
|
|
20931
|
+
const sandbox = asReasonixRecord(this.toml.sandbox);
|
|
20932
|
+
const agentPlanMode = pickReasonixKeys(this.toml.agent, REASONIX_OVERRIDE_AGENT_KEYS);
|
|
20933
|
+
const reasonixOverride = {};
|
|
20934
|
+
if (Object.keys(sandbox).length > 0) reasonixOverride.sandbox = sandbox;
|
|
20935
|
+
if (Object.keys(agentPlanMode).length > 0) reasonixOverride.agent = agentPlanMode;
|
|
20936
|
+
const result = { ...config };
|
|
20937
|
+
if (Object.keys(reasonixOverride).length > 0) result.reasonix = reasonixOverride;
|
|
20938
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
|
|
19905
20939
|
}
|
|
19906
20940
|
validate() {
|
|
19907
20941
|
try {
|
|
@@ -20211,6 +21245,8 @@ function isPermissionAction(value) {
|
|
|
20211
21245
|
const TAKT_PROVIDER_KEY = "provider";
|
|
20212
21246
|
const TAKT_PROVIDER_PROFILES_KEY = "provider_profiles";
|
|
20213
21247
|
const TAKT_DEFAULT_PERMISSION_MODE_KEY = "default_permission_mode";
|
|
21248
|
+
const TAKT_STEP_PERMISSION_OVERRIDES_KEY = "step_permission_overrides";
|
|
21249
|
+
const TAKT_PROVIDER_OPTIONS_KEY = "provider_options";
|
|
20214
21250
|
const TAKT_DEFAULT_PROVIDER = "claude";
|
|
20215
21251
|
const CATCH_ALL_PATTERN = "*";
|
|
20216
21252
|
/**
|
|
@@ -20240,10 +21276,16 @@ const CATCH_ALL_PATTERN = "*";
|
|
|
20240
21276
|
* `bash: { "*": "deny" }`. These round-trip the generate mapping.
|
|
20241
21277
|
*
|
|
20242
21278
|
* Both project and global scope are supported. The shared config is merged in
|
|
20243
|
-
* place:
|
|
20244
|
-
*
|
|
20245
|
-
*
|
|
20246
|
-
*
|
|
21279
|
+
* place: `provider_profiles.<provider>.default_permission_mode` is set from the
|
|
21280
|
+
* derived mode; every other provider profile and all other top-level keys are
|
|
21281
|
+
* preserved. The file is never deleted.
|
|
21282
|
+
*
|
|
21283
|
+
* Two Takt-specific surfaces with no canonical category round-trip through the
|
|
21284
|
+
* `takt` override (see `TaktPermissionsOverrideSchema`):
|
|
21285
|
+
* `step_permission_overrides` (a per-step mode map inside the active provider
|
|
21286
|
+
* profile, layered on top of `default_permission_mode`) and `provider_options`
|
|
21287
|
+
* (a top-level per-provider sandbox/network table). Both are authored on
|
|
21288
|
+
* generate and re-extracted on import.
|
|
20247
21289
|
*/
|
|
20248
21290
|
var TaktPermissions = class TaktPermissions extends ToolPermissions {
|
|
20249
21291
|
constructor(params) {
|
|
@@ -20275,37 +21317,62 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
|
|
|
20275
21317
|
}
|
|
20276
21318
|
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false }) {
|
|
20277
21319
|
const paths = TaktPermissions.getSettablePaths({ global });
|
|
20278
|
-
const
|
|
21320
|
+
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
21321
|
+
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
21322
|
+
const config = parseSharedConfig({
|
|
21323
|
+
format: "yaml",
|
|
21324
|
+
fileContent: existingContent,
|
|
21325
|
+
filePath,
|
|
21326
|
+
invalidRootPolicy: "error"
|
|
21327
|
+
});
|
|
21328
|
+
const rulesyncJson = rulesyncPermissions.getJson();
|
|
20279
21329
|
const provider = resolveActiveProvider(config);
|
|
20280
|
-
const mode = deriveTaktPermissionMode(
|
|
20281
|
-
const
|
|
20282
|
-
const
|
|
20283
|
-
const
|
|
20284
|
-
|
|
20285
|
-
[TAKT_PROVIDER_PROFILES_KEY]: {
|
|
20286
|
-
|
|
20287
|
-
[
|
|
20288
|
-
|
|
20289
|
-
|
|
20290
|
-
}
|
|
20291
|
-
}
|
|
21330
|
+
const mode = deriveTaktPermissionMode(rulesyncJson);
|
|
21331
|
+
const override = isPlainObject(rulesyncJson.takt) ? rulesyncJson.takt : void 0;
|
|
21332
|
+
const stepOverrides = isPlainObject(override?.[TAKT_STEP_PERMISSION_OVERRIDES_KEY]) ? override[TAKT_STEP_PERMISSION_OVERRIDES_KEY] : void 0;
|
|
21333
|
+
const overrideProviderOptions = isPlainObject(override?.[TAKT_PROVIDER_OPTIONS_KEY]) ? override[TAKT_PROVIDER_OPTIONS_KEY] : void 0;
|
|
21334
|
+
const patch = {
|
|
21335
|
+
[TAKT_PROVIDER_PROFILES_KEY]: { [provider]: {
|
|
21336
|
+
[TAKT_DEFAULT_PERMISSION_MODE_KEY]: mode,
|
|
21337
|
+
...stepOverrides !== void 0 && { [TAKT_STEP_PERMISSION_OVERRIDES_KEY]: stepOverrides }
|
|
21338
|
+
} },
|
|
21339
|
+
...overrideProviderOptions !== void 0 && { [TAKT_PROVIDER_OPTIONS_KEY]: overrideProviderOptions }
|
|
20292
21340
|
};
|
|
20293
21341
|
return new TaktPermissions({
|
|
20294
21342
|
outputRoot,
|
|
20295
21343
|
relativeDirPath: paths.relativeDirPath,
|
|
20296
21344
|
relativeFilePath: paths.relativeFilePath,
|
|
20297
|
-
fileContent:
|
|
21345
|
+
fileContent: applySharedConfigPatch({
|
|
21346
|
+
fileKey: TAKT_CONFIG_SHARED_FILE_KEY,
|
|
21347
|
+
feature: "permissions",
|
|
21348
|
+
existingContent,
|
|
21349
|
+
patch,
|
|
21350
|
+
filePath
|
|
21351
|
+
}),
|
|
20298
21352
|
validate: true,
|
|
20299
21353
|
global
|
|
20300
21354
|
});
|
|
20301
21355
|
}
|
|
20302
21356
|
toRulesyncPermissions() {
|
|
20303
|
-
const config =
|
|
21357
|
+
const config = parseSharedConfig({
|
|
21358
|
+
format: "yaml",
|
|
21359
|
+
fileContent: this.getFileContent(),
|
|
21360
|
+
filePath: join(this.getRelativeDirPath(), this.getRelativeFilePath()),
|
|
21361
|
+
invalidRootPolicy: "error"
|
|
21362
|
+
});
|
|
20304
21363
|
const provider = resolveActiveProvider(config);
|
|
20305
21364
|
const profiles = isPlainObject(config[TAKT_PROVIDER_PROFILES_KEY]) ? config[TAKT_PROVIDER_PROFILES_KEY] : {};
|
|
20306
|
-
const
|
|
21365
|
+
const profile = isPlainObject(profiles[provider]) ? profiles[provider] : {};
|
|
21366
|
+
const mode = profile[TAKT_DEFAULT_PERMISSION_MODE_KEY];
|
|
20307
21367
|
const rulesyncConfig = taktModeToRulesyncConfig(mode);
|
|
20308
|
-
|
|
21368
|
+
const stepOverrides = isPlainObject(profile[TAKT_STEP_PERMISSION_OVERRIDES_KEY]) ? profile[TAKT_STEP_PERMISSION_OVERRIDES_KEY] : void 0;
|
|
21369
|
+
const providerOptions = isPlainObject(config[TAKT_PROVIDER_OPTIONS_KEY]) ? config[TAKT_PROVIDER_OPTIONS_KEY] : void 0;
|
|
21370
|
+
const taktOverride = {};
|
|
21371
|
+
if (stepOverrides && Object.keys(stepOverrides).length > 0) taktOverride[TAKT_STEP_PERMISSION_OVERRIDES_KEY] = stepOverrides;
|
|
21372
|
+
if (providerOptions && Object.keys(providerOptions).length > 0) taktOverride[TAKT_PROVIDER_OPTIONS_KEY] = providerOptions;
|
|
21373
|
+
const result = { ...rulesyncConfig };
|
|
21374
|
+
if (Object.keys(taktOverride).length > 0) result.takt = taktOverride;
|
|
21375
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
|
|
20309
21376
|
}
|
|
20310
21377
|
validate() {
|
|
20311
21378
|
return {
|
|
@@ -20465,6 +21532,7 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
|
|
|
20465
21532
|
if (deny.size > 0) nextTool.denylist = [...deny].toSorted();
|
|
20466
21533
|
tools[vibeToolName] = nextTool;
|
|
20467
21534
|
}
|
|
21535
|
+
applyVibeSensitivePatterns(tools, rulesyncPermissions.getJson().vibe);
|
|
20468
21536
|
config.tools = tools;
|
|
20469
21537
|
if (enabledTools.size > 0) config.enabled_tools = [...enabledTools].toSorted();
|
|
20470
21538
|
else delete config.enabled_tools;
|
|
@@ -20481,16 +21549,25 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
|
|
|
20481
21549
|
}
|
|
20482
21550
|
toRulesyncPermissions() {
|
|
20483
21551
|
const permission = {};
|
|
21552
|
+
const vibeOverridePermission = {};
|
|
20484
21553
|
for (const tool of toStringArray(this.toml.enabled_tools)) ensurePermission(permission, toCanonicalToolName$1(tool))["*"] = "allow";
|
|
20485
21554
|
for (const tool of toStringArray(this.toml.disabled_tools)) ensurePermission(permission, toCanonicalToolName$1(tool))["*"] = "deny";
|
|
20486
21555
|
for (const [vibeToolName, toolConfig] of Object.entries(toVibeToolsRecord(this.toml.tools))) {
|
|
20487
|
-
const
|
|
21556
|
+
const category = toCanonicalToolName$1(vibeToolName);
|
|
21557
|
+
const rules = ensurePermission(permission, category);
|
|
20488
21558
|
const action = fromVibePermission(toolConfig.permission);
|
|
20489
21559
|
if (action !== void 0) rules["*"] = action;
|
|
20490
21560
|
for (const pattern of toStringArray(toolConfig.allow ?? toolConfig.allowlist)) rules[pattern] = "allow";
|
|
20491
21561
|
for (const pattern of toStringArray(toolConfig.deny ?? toolConfig.denylist)) rules[pattern] = "deny";
|
|
21562
|
+
const sensitivePatterns = toStringArray(toolConfig.sensitive_patterns);
|
|
21563
|
+
if (sensitivePatterns.length > 0) vibeOverridePermission[category] = { sensitive_patterns: sensitivePatterns };
|
|
20492
21564
|
}
|
|
20493
|
-
|
|
21565
|
+
for (const [category, rules] of Object.entries(permission)) if (Object.keys(rules).length === 0) delete permission[category];
|
|
21566
|
+
const json = Object.keys(vibeOverridePermission).length > 0 ? {
|
|
21567
|
+
permission,
|
|
21568
|
+
vibe: { permission: vibeOverridePermission }
|
|
21569
|
+
} : { permission };
|
|
21570
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(json, null, 2) });
|
|
20494
21571
|
}
|
|
20495
21572
|
validate() {
|
|
20496
21573
|
try {
|
|
@@ -20517,6 +21594,22 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
|
|
|
20517
21594
|
});
|
|
20518
21595
|
}
|
|
20519
21596
|
};
|
|
21597
|
+
/**
|
|
21598
|
+
* Apply the Vibe-scoped override's per-tool `sensitive_patterns` (patterns that
|
|
21599
|
+
* escalate to ASK even when the base permission is ALWAYS). rulesync owns this
|
|
21600
|
+
* list for any category the override names: a present list is set, an empty
|
|
21601
|
+
* one clears it. Categories not named keep whatever the existing file had.
|
|
21602
|
+
*/
|
|
21603
|
+
function applyVibeSensitivePatterns(tools, vibeOverride) {
|
|
21604
|
+
for (const [category, toolOverride] of Object.entries(vibeOverride?.permission ?? {})) {
|
|
21605
|
+
const vibeToolName = toVibeToolName(category);
|
|
21606
|
+
const nextTool = toVibeToolConfig(tools[vibeToolName]);
|
|
21607
|
+
const patterns = toStringArray(toolOverride.sensitive_patterns);
|
|
21608
|
+
if (patterns.length > 0) nextTool.sensitive_patterns = [...patterns].toSorted();
|
|
21609
|
+
else delete nextTool.sensitive_patterns;
|
|
21610
|
+
tools[vibeToolName] = nextTool;
|
|
21611
|
+
}
|
|
21612
|
+
}
|
|
20520
21613
|
function parseVibeConfig(fileContent) {
|
|
20521
21614
|
const parsed = smolToml.parse(fileContent || smolToml.stringify({}));
|
|
20522
21615
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
@@ -20561,6 +21654,11 @@ function ensurePermission(permission, category) {
|
|
|
20561
21654
|
const WARP_GLOBAL_ONLY_MESSAGE = "Warp permissions are global-only; use --global to sync Warp's settings.toml";
|
|
20562
21655
|
const ALLOWLIST_KEY = "agent_mode_command_execution_allowlist";
|
|
20563
21656
|
const DENYLIST_KEY = "agent_mode_command_execution_denylist";
|
|
21657
|
+
const WARP_OVERRIDE_KEYS = [
|
|
21658
|
+
"agent_mode_coding_permissions",
|
|
21659
|
+
"agent_mode_coding_file_read_allowlist",
|
|
21660
|
+
"agent_mode_execute_readonly_commands"
|
|
21661
|
+
];
|
|
20564
21662
|
/**
|
|
20565
21663
|
* Warp's `settings.toml` lives in a different directory per platform (Stable
|
|
20566
21664
|
* channel). The home directory is resolved by the processor through
|
|
@@ -20596,8 +21694,16 @@ function warpSettingsDir() {
|
|
|
20596
21694
|
* patterns as regexes when targeting Warp (mirrors the Zed permissions
|
|
20597
21695
|
* adapter). Warp has no per-command "ask" list, so `ask` rules are dropped; and
|
|
20598
21696
|
* the command lists only model shell commands, so non-`bash` categories are
|
|
20599
|
-
* skipped (with a warning when they carry `deny` rules).
|
|
20600
|
-
*
|
|
21697
|
+
* skipped (with a warning when they carry `deny` rules).
|
|
21698
|
+
*
|
|
21699
|
+
* Warp's `[agents.profiles]` table also exposes file-read/read-only autonomy
|
|
21700
|
+
* knobs that do not fit the canonical `allow | ask | deny` per-command model:
|
|
21701
|
+
* `agent_mode_coding_permissions`, `agent_mode_coding_file_read_allowlist`, and
|
|
21702
|
+
* `agent_mode_execute_readonly_commands`. These are authored and round-tripped
|
|
21703
|
+
* through the `warp` override namespace (see `WarpPermissionsOverrideSchema`):
|
|
21704
|
+
* on **import** they are lifted from `settings.toml` into the override, and on
|
|
21705
|
+
* **export** they are merged back into `[agents.profiles]` (the override wins).
|
|
21706
|
+
* MCP allow/deny is a separate surface not modeled here.
|
|
20601
21707
|
*
|
|
20602
21708
|
* The `settings.toml` file holds all of Warp's settings, so the
|
|
20603
21709
|
* `[agents.profiles]` block is merged in place and the file is never deleted.
|
|
@@ -20642,12 +21748,15 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
|
|
|
20642
21748
|
} catch (error) {
|
|
20643
21749
|
throw new Error(`Failed to parse existing Warp settings at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
20644
21750
|
}
|
|
21751
|
+
const config = rulesyncPermissions.getJson();
|
|
20645
21752
|
const { allow, deny } = convertRulesyncToWarpPermissions({
|
|
20646
|
-
config
|
|
21753
|
+
config,
|
|
20647
21754
|
logger
|
|
20648
21755
|
});
|
|
20649
21756
|
const agents = isRecord(settings.agents) ? { ...settings.agents } : {};
|
|
20650
21757
|
const profiles = isRecord(agents.profiles) ? { ...agents.profiles } : {};
|
|
21758
|
+
const override = config.warp;
|
|
21759
|
+
if (isRecord(override)) Object.assign(profiles, override);
|
|
20651
21760
|
const mergedAllow = uniq(allow.toSorted());
|
|
20652
21761
|
const mergedDeny = uniq(deny.toSorted());
|
|
20653
21762
|
if (mergedAllow.length > 0) profiles[ALLOWLIST_KEY] = mergedAllow;
|
|
@@ -20678,7 +21787,11 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
|
|
|
20678
21787
|
allow: isStringArray(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
|
|
20679
21788
|
deny: isStringArray(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
|
|
20680
21789
|
});
|
|
20681
|
-
|
|
21790
|
+
const warpOverride = {};
|
|
21791
|
+
for (const key of WARP_OVERRIDE_KEYS) if (profiles[key] !== void 0) warpOverride[key] = profiles[key];
|
|
21792
|
+
const result = { ...config };
|
|
21793
|
+
if (Object.keys(warpOverride).length > 0) result.warp = warpOverride;
|
|
21794
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
|
|
20682
21795
|
}
|
|
20683
21796
|
validate() {
|
|
20684
21797
|
return {
|
|
@@ -24044,8 +25157,9 @@ const DevinSkillFrontmatterSchema = z.looseObject({
|
|
|
24044
25157
|
* Represents a Devin (now Devin Desktop) skill directory.
|
|
24045
25158
|
* Devin supports skills in both project mode under .devin/skills/
|
|
24046
25159
|
* (preferred since the Devin Desktop rebrand; .devin/skills/ is the legacy
|
|
24047
|
-
* fallback the tool still reads) and global mode under
|
|
24048
|
-
* (
|
|
25160
|
+
* fallback the tool still reads) and global mode under the Devin-native
|
|
25161
|
+
* ~/.config/devin/skills/ (consistent with Devin's global agents/rules paths;
|
|
25162
|
+
* the legacy channel-dependent ~/.codeium/<channel>/skills/ is no longer emitted).
|
|
24049
25163
|
*/
|
|
24050
25164
|
var DevinSkill = class DevinSkill extends ToolSkill {
|
|
24051
25165
|
constructor({ outputRoot = process.cwd(), relativeDirPath = DevinSkill.getSettablePaths().relativeDirPath, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
|
|
@@ -24067,7 +25181,7 @@ var DevinSkill = class DevinSkill extends ToolSkill {
|
|
|
24067
25181
|
}
|
|
24068
25182
|
}
|
|
24069
25183
|
static getSettablePaths({ global = false } = {}) {
|
|
24070
|
-
if (global) return { relativeDirPath:
|
|
25184
|
+
if (global) return { relativeDirPath: DEVIN_GLOBAL_SKILLS_DIR_PATH };
|
|
24071
25185
|
return { relativeDirPath: DEVIN_SKILLS_DIR_PATH };
|
|
24072
25186
|
}
|
|
24073
25187
|
getFrontmatter() {
|
|
@@ -27126,7 +28240,7 @@ const QwencodeSubagentFrontmatterSchema = z.looseObject({
|
|
|
27126
28240
|
disallowedTools: z.optional(z.array(z.string())),
|
|
27127
28241
|
maxTurns: z.optional(z.number()),
|
|
27128
28242
|
color: z.optional(z.string()),
|
|
27129
|
-
mcpServers: z.optional(z.array(z.string())),
|
|
28243
|
+
mcpServers: z.optional(z.union([z.array(z.string()), z.record(z.string(), z.looseObject({}))])),
|
|
27130
28244
|
hooks: z.optional(z.unknown())
|
|
27131
28245
|
});
|
|
27132
28246
|
var QwencodeSubagent = class QwencodeSubagent extends ToolSubagent {
|
|
@@ -29153,14 +30267,21 @@ def register(ctx):
|
|
|
29153
30267
|
`;
|
|
29154
30268
|
}
|
|
29155
30269
|
function getEnabledPluginConfigContent(currentContent) {
|
|
29156
|
-
const config =
|
|
30270
|
+
const config = parseSharedConfig({
|
|
30271
|
+
format: "yaml",
|
|
30272
|
+
fileContent: currentContent
|
|
30273
|
+
});
|
|
29157
30274
|
const plugins = config.plugins && typeof config.plugins === "object" ? config.plugins : {};
|
|
29158
30275
|
const enabled = Array.isArray(plugins.enabled) ? plugins.enabled : [];
|
|
29159
|
-
|
|
29160
|
-
|
|
29161
|
-
|
|
29162
|
-
|
|
29163
|
-
|
|
30276
|
+
return applySharedConfigPatch({
|
|
30277
|
+
fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
|
|
30278
|
+
feature: "subagents",
|
|
30279
|
+
existingContent: currentContent,
|
|
30280
|
+
patch: { plugins: {
|
|
30281
|
+
...plugins,
|
|
30282
|
+
enabled: Array.from(/* @__PURE__ */ new Set([...enabled, "rulesync-subagents"]))
|
|
30283
|
+
} }
|
|
30284
|
+
});
|
|
29164
30285
|
}
|
|
29165
30286
|
function getSubagentSpec(rulesyncSubagent) {
|
|
29166
30287
|
const json = rulesyncSubagent.getFrontmatter();
|
|
@@ -29246,6 +30367,17 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
|
|
|
29246
30367
|
static getSettablePaths() {
|
|
29247
30368
|
return { relativeDirPath: HERMESAGENT_RULESYNC_SUBAGENTS_DIR_PATH };
|
|
29248
30369
|
}
|
|
30370
|
+
/**
|
|
30371
|
+
* Beyond the subagent spec files, generation also read-modify-writes the
|
|
30372
|
+
* shared `~/.hermes/config.yaml` (enabling the `rulesync-subagents` plugin),
|
|
30373
|
+
* so the write must be declared for the shared-file order derivation.
|
|
30374
|
+
*/
|
|
30375
|
+
static getExtraSharedWritePaths() {
|
|
30376
|
+
return [{
|
|
30377
|
+
relativeDirPath: HERMESAGENT_GLOBAL_DIR,
|
|
30378
|
+
relativeFilePath: basename(HERMESAGENT_CONFIG_FILE_PATH)
|
|
30379
|
+
}];
|
|
30380
|
+
}
|
|
29249
30381
|
static getSettablePathsForRulesyncSubagent(rulesyncSubagent) {
|
|
29250
30382
|
return [join(HERMESAGENT_RULESYNC_SUBAGENTS_DIR_PATH, `${subagentSlug(rulesyncSubagent.getRelativePathFromCwd())}.json`)];
|
|
29251
30383
|
}
|
|
@@ -30984,7 +32116,9 @@ const RulesyncRuleFrontmatterSchema = z.object({
|
|
|
30984
32116
|
})),
|
|
30985
32117
|
kiro: z.optional(z.looseObject({
|
|
30986
32118
|
inclusion: z.optional(z.string()),
|
|
30987
|
-
fileMatchPattern: z.optional(z.union([z.string(), z.array(z.string())]))
|
|
32119
|
+
fileMatchPattern: z.optional(z.union([z.string(), z.array(z.string())])),
|
|
32120
|
+
name: z.optional(z.string()),
|
|
32121
|
+
description: z.optional(z.string())
|
|
30988
32122
|
})),
|
|
30989
32123
|
takt: z.optional(z.looseObject({
|
|
30990
32124
|
name: z.optional(z.string()),
|
|
@@ -33900,7 +35034,8 @@ function toFileMatchPattern(globs) {
|
|
|
33900
35034
|
*
|
|
33901
35035
|
* Precedence:
|
|
33902
35036
|
* 1. An explicit `kiro.inclusion` block round-trips as-is (with `fileMatchPattern`
|
|
33903
|
-
* taken from the block, or derived from `globs` when omitted for `fileMatch
|
|
35037
|
+
* taken from the block, or derived from `globs` when omitted for `fileMatch`;
|
|
35038
|
+
* and with the companion `name`/`description` carried through for `auto`).
|
|
33904
35039
|
* 2. Otherwise specific (non-wildcard) globs map to `fileMatch`, scoping the rule
|
|
33905
35040
|
* to matching files instead of leaving it implicitly always-on.
|
|
33906
35041
|
* 3. Otherwise the rule stays `always` — represented by omitting the block so the
|
|
@@ -33918,6 +35053,11 @@ function deriveKiroInclusion({ kiro, globs }) {
|
|
|
33918
35053
|
fileMatchPattern
|
|
33919
35054
|
} : { inclusion: "fileMatch" };
|
|
33920
35055
|
}
|
|
35056
|
+
if (kiro.inclusion === "auto") return {
|
|
35057
|
+
inclusion: "auto",
|
|
35058
|
+
...kiro.name !== void 0 ? { name: kiro.name } : {},
|
|
35059
|
+
...kiro.description !== void 0 ? { description: kiro.description } : {}
|
|
35060
|
+
};
|
|
33921
35061
|
return { inclusion: kiro.inclusion };
|
|
33922
35062
|
}
|
|
33923
35063
|
const fileMatchPattern = toFileMatchPattern(specificGlobs);
|
|
@@ -33989,6 +35129,8 @@ var KiroRule = class KiroRule extends ToolRule {
|
|
|
33989
35129
|
const rawPattern = frontmatter.fileMatchPattern;
|
|
33990
35130
|
const fileMatchPattern = Array.isArray(rawPattern) ? rawPattern.filter((p) => typeof p === "string") : typeof rawPattern === "string" ? rawPattern : void 0;
|
|
33991
35131
|
const globs = inclusion === "fileMatch" ? fileMatchPattern === void 0 ? [] : Array.isArray(fileMatchPattern) ? fileMatchPattern : [fileMatchPattern] : [];
|
|
35132
|
+
const name = typeof frontmatter.name === "string" ? frontmatter.name : void 0;
|
|
35133
|
+
const description = typeof frontmatter.description === "string" ? frontmatter.description : void 0;
|
|
33992
35134
|
return new RulesyncRule({
|
|
33993
35135
|
outputRoot: process.cwd(),
|
|
33994
35136
|
relativeDirPath: RulesyncRule.getSettablePaths().recommended.relativeDirPath,
|
|
@@ -33999,7 +35141,9 @@ var KiroRule = class KiroRule extends ToolRule {
|
|
|
33999
35141
|
globs,
|
|
34000
35142
|
kiro: {
|
|
34001
35143
|
inclusion,
|
|
34002
|
-
...fileMatchPattern !== void 0 ? { fileMatchPattern } : {}
|
|
35144
|
+
...fileMatchPattern !== void 0 ? { fileMatchPattern } : {},
|
|
35145
|
+
...inclusion === "auto" && name !== void 0 ? { name } : {},
|
|
35146
|
+
...inclusion === "auto" && description !== void 0 ? { description } : {}
|
|
34003
35147
|
}
|
|
34004
35148
|
},
|
|
34005
35149
|
body
|
|
@@ -36173,6 +37317,197 @@ function buildPermissionsStrategy(ctx) {
|
|
|
36173
37317
|
};
|
|
36174
37318
|
}
|
|
36175
37319
|
//#endregion
|
|
37320
|
+
//#region src/types/processor-registry.ts
|
|
37321
|
+
const PROCESSOR_REGISTRY = [
|
|
37322
|
+
{
|
|
37323
|
+
feature: "rules",
|
|
37324
|
+
processor: RulesProcessor,
|
|
37325
|
+
schema: RulesProcessorToolTargetSchema,
|
|
37326
|
+
factory: toolRuleFactories
|
|
37327
|
+
},
|
|
37328
|
+
{
|
|
37329
|
+
feature: "ignore",
|
|
37330
|
+
processor: IgnoreProcessor,
|
|
37331
|
+
schema: IgnoreProcessorToolTargetSchema,
|
|
37332
|
+
factory: toolIgnoreFactories
|
|
37333
|
+
},
|
|
37334
|
+
{
|
|
37335
|
+
feature: "mcp",
|
|
37336
|
+
processor: McpProcessor,
|
|
37337
|
+
schema: McpProcessorToolTargetSchema,
|
|
37338
|
+
factory: toolMcpFactories
|
|
37339
|
+
},
|
|
37340
|
+
{
|
|
37341
|
+
feature: "commands",
|
|
37342
|
+
processor: CommandsProcessor,
|
|
37343
|
+
schema: CommandsProcessorToolTargetSchema,
|
|
37344
|
+
factory: toolCommandFactories
|
|
37345
|
+
},
|
|
37346
|
+
{
|
|
37347
|
+
feature: "subagents",
|
|
37348
|
+
processor: SubagentsProcessor,
|
|
37349
|
+
schema: SubagentsProcessorToolTargetSchema,
|
|
37350
|
+
factory: toolSubagentFactories
|
|
37351
|
+
},
|
|
37352
|
+
{
|
|
37353
|
+
feature: "skills",
|
|
37354
|
+
processor: SkillsProcessor,
|
|
37355
|
+
schema: SkillsProcessorToolTargetSchema,
|
|
37356
|
+
factory: toolSkillFactories
|
|
37357
|
+
},
|
|
37358
|
+
{
|
|
37359
|
+
feature: "hooks",
|
|
37360
|
+
processor: HooksProcessor,
|
|
37361
|
+
schema: HooksProcessorToolTargetSchema,
|
|
37362
|
+
factory: toolHooksFactories
|
|
37363
|
+
},
|
|
37364
|
+
{
|
|
37365
|
+
feature: "permissions",
|
|
37366
|
+
processor: PermissionsProcessor,
|
|
37367
|
+
schema: PermissionsProcessorToolTargetSchema,
|
|
37368
|
+
factory: toolPermissionsFactories
|
|
37369
|
+
}
|
|
37370
|
+
];
|
|
37371
|
+
const getProcessorRegistryEntry = (feature) => {
|
|
37372
|
+
const entry = PROCESSOR_REGISTRY.find((e) => e.feature === feature);
|
|
37373
|
+
if (!entry) throw new Error(`No processor registered for feature: ${feature}`);
|
|
37374
|
+
return entry;
|
|
37375
|
+
};
|
|
37376
|
+
//#endregion
|
|
37377
|
+
//#region src/lib/shared-file-derive.ts
|
|
37378
|
+
/**
|
|
37379
|
+
* The single declaration of the cross-feature write order for shared
|
|
37380
|
+
* (read-modify-write) config files: when two features write the same on-disk
|
|
37381
|
+
* file, the earlier one writes first and the later one merges on top, so the
|
|
37382
|
+
* later feature's conflict policy decides what survives (e.g. `permissions`
|
|
37383
|
+
* overriding `ignore`-derived `Read(...)` denies in `.claude/settings.json`).
|
|
37384
|
+
* The generation step graph's `dependsOn` edges are derived from this list
|
|
37385
|
+
* plus the registry's `getSettablePaths` declarations — adding a tool or a
|
|
37386
|
+
* shared path never requires touching the graph by hand.
|
|
37387
|
+
*/
|
|
37388
|
+
const SHARED_WRITE_FEATURE_ORDER = [
|
|
37389
|
+
"ignore",
|
|
37390
|
+
"subagents",
|
|
37391
|
+
"mcp",
|
|
37392
|
+
"hooks",
|
|
37393
|
+
"permissions",
|
|
37394
|
+
"rules"
|
|
37395
|
+
];
|
|
37396
|
+
const SHARED_WRITE_FEATURES = new Set(SHARED_WRITE_FEATURE_ORDER);
|
|
37397
|
+
const TARGETS_NOT_DERIVED = /* @__PURE__ */ new Set(["augmentcode-legacy", "claudecode-legacy"]);
|
|
37398
|
+
const sharedFileKey = (path) => {
|
|
37399
|
+
const dir = path.relativeDirPath.replace(/\\/g, "/").replace(/\/$/, "");
|
|
37400
|
+
const file = path.relativeFilePath.replace(/\\/g, "/");
|
|
37401
|
+
return dir === "" || dir === "." ? file : `${dir}/${file}`;
|
|
37402
|
+
};
|
|
37403
|
+
const settablePathsForScope = (cls, global) => {
|
|
37404
|
+
const paths = [];
|
|
37405
|
+
let settable;
|
|
37406
|
+
try {
|
|
37407
|
+
settable = cls.getSettablePaths?.({ global });
|
|
37408
|
+
} catch {
|
|
37409
|
+
return paths;
|
|
37410
|
+
}
|
|
37411
|
+
if (settable?.relativeFilePath) paths.push({
|
|
37412
|
+
relativeDirPath: settable.relativeDirPath ?? ".",
|
|
37413
|
+
relativeFilePath: settable.relativeFilePath
|
|
37414
|
+
});
|
|
37415
|
+
if (settable?.root) {
|
|
37416
|
+
paths.push(settable.root);
|
|
37417
|
+
for (const alt of settable.alternativeRoots ?? []) paths.push(alt);
|
|
37418
|
+
}
|
|
37419
|
+
for (const path of cls.getExtraSharedWritePaths?.({ global }) ?? []) if (path.relativeFilePath) paths.push(path);
|
|
37420
|
+
return paths;
|
|
37421
|
+
};
|
|
37422
|
+
const collectFactoryPaths = (factory) => [...settablePathsForScope(factory.class, false), ...settablePathsForScope(factory.class, true)];
|
|
37423
|
+
/**
|
|
37424
|
+
* Derive, from the processor registry, every on-disk file that two or more
|
|
37425
|
+
* features read-modify-write. Source of truth the generation step graph's
|
|
37426
|
+
* `writesSharedFile` declarations must match.
|
|
37427
|
+
*/
|
|
37428
|
+
const deriveSharedFileWriters = () => {
|
|
37429
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
37430
|
+
const pathByKey = /* @__PURE__ */ new Map();
|
|
37431
|
+
for (const entry of PROCESSOR_REGISTRY) {
|
|
37432
|
+
if (!SHARED_WRITE_FEATURES.has(entry.feature)) continue;
|
|
37433
|
+
const factories = entry.factory;
|
|
37434
|
+
for (const [tool, factory] of factories) {
|
|
37435
|
+
if (TARGETS_NOT_DERIVED.has(tool)) continue;
|
|
37436
|
+
for (const path of collectFactoryPaths(factory)) {
|
|
37437
|
+
const key = sharedFileKey(path);
|
|
37438
|
+
if (!pathByKey.has(key)) pathByKey.set(key, path);
|
|
37439
|
+
let features = byKey.get(key);
|
|
37440
|
+
if (!features) {
|
|
37441
|
+
features = /* @__PURE__ */ new Map();
|
|
37442
|
+
byKey.set(key, features);
|
|
37443
|
+
}
|
|
37444
|
+
let tools = features.get(entry.feature);
|
|
37445
|
+
if (!tools) {
|
|
37446
|
+
tools = /* @__PURE__ */ new Set();
|
|
37447
|
+
features.set(entry.feature, tools);
|
|
37448
|
+
}
|
|
37449
|
+
tools.add(tool);
|
|
37450
|
+
}
|
|
37451
|
+
}
|
|
37452
|
+
}
|
|
37453
|
+
const writers = [];
|
|
37454
|
+
for (const [key, features] of byKey) {
|
|
37455
|
+
if (features.size < 2) continue;
|
|
37456
|
+
const path = pathByKey.get(key);
|
|
37457
|
+
const toolsByFeature = /* @__PURE__ */ new Map();
|
|
37458
|
+
for (const [feature, tools] of features) toolsByFeature.set(feature, [...tools].toSorted());
|
|
37459
|
+
writers.push({
|
|
37460
|
+
key,
|
|
37461
|
+
relativeDirPath: path.relativeDirPath,
|
|
37462
|
+
relativeFilePath: path.relativeFilePath,
|
|
37463
|
+
features: [...features.keys()].toSorted(),
|
|
37464
|
+
toolsByFeature
|
|
37465
|
+
});
|
|
37466
|
+
}
|
|
37467
|
+
return writers.toSorted((a, b) => a.key.localeCompare(b.key));
|
|
37468
|
+
};
|
|
37469
|
+
/**
|
|
37470
|
+
* Derive, per shared-write feature, the shared files it writes and the
|
|
37471
|
+
* `dependsOn` edges that fix a safe write order: for every shared file, each
|
|
37472
|
+
* writer depends on all writers that precede it in
|
|
37473
|
+
* {@link SHARED_WRITE_FEATURE_ORDER}. This is the source the generation step
|
|
37474
|
+
* graph consumes, so registry changes (a new tool, a new settable path)
|
|
37475
|
+
* propagate into the execution order without a hand-maintained declaration.
|
|
37476
|
+
*
|
|
37477
|
+
* @throws Error if a feature writes a shared file but has no position in
|
|
37478
|
+
* `SHARED_WRITE_FEATURE_ORDER` — ordering it is a deliberate decision about
|
|
37479
|
+
* whose merge policy wins, so it must be made explicitly there.
|
|
37480
|
+
*/
|
|
37481
|
+
const deriveSharedWriteSteps = () => {
|
|
37482
|
+
const orderIndex = new Map(SHARED_WRITE_FEATURE_ORDER.map((feature, index) => [feature, index]));
|
|
37483
|
+
const filesByFeature = /* @__PURE__ */ new Map();
|
|
37484
|
+
const depsByFeature = /* @__PURE__ */ new Map();
|
|
37485
|
+
for (const writer of deriveSharedFileWriters()) {
|
|
37486
|
+
for (const feature of writer.features) if (!orderIndex.has(feature)) throw new Error(`Feature '${feature}' writes the shared file '${writer.key}' but has no position in SHARED_WRITE_FEATURE_ORDER. Decide where its writes merge relative to the other features and add it to the order.`);
|
|
37487
|
+
const ordered = [...writer.features].toSorted((a, b) => orderIndex.get(a) - orderIndex.get(b));
|
|
37488
|
+
for (const [position, feature] of ordered.entries()) {
|
|
37489
|
+
let files = filesByFeature.get(feature);
|
|
37490
|
+
if (!files) {
|
|
37491
|
+
files = /* @__PURE__ */ new Set();
|
|
37492
|
+
filesByFeature.set(feature, files);
|
|
37493
|
+
}
|
|
37494
|
+
files.add(writer.key);
|
|
37495
|
+
let deps = depsByFeature.get(feature);
|
|
37496
|
+
if (!deps) {
|
|
37497
|
+
deps = /* @__PURE__ */ new Set();
|
|
37498
|
+
depsByFeature.set(feature, deps);
|
|
37499
|
+
}
|
|
37500
|
+
for (const earlier of ordered.slice(0, position)) deps.add(earlier);
|
|
37501
|
+
}
|
|
37502
|
+
}
|
|
37503
|
+
const steps = /* @__PURE__ */ new Map();
|
|
37504
|
+
for (const [feature, files] of filesByFeature) steps.set(feature, {
|
|
37505
|
+
writesSharedFile: [...files].toSorted(),
|
|
37506
|
+
dependsOn: [...depsByFeature.get(feature) ?? []].toSorted((a, b) => orderIndex.get(a) - orderIndex.get(b))
|
|
37507
|
+
});
|
|
37508
|
+
return steps;
|
|
37509
|
+
};
|
|
37510
|
+
//#endregion
|
|
36176
37511
|
//#region src/lib/generate.ts
|
|
36177
37512
|
async function processFeatureGeneration(params) {
|
|
36178
37513
|
const { config, processor, toolFiles, skipFilePaths } = params;
|
|
@@ -36322,98 +37657,53 @@ function resolveExecutionOrder(steps) {
|
|
|
36322
37657
|
if (ordered.length !== steps.length) throw new Error("Generation steps contain a cyclic 'dependsOn' dependency.");
|
|
36323
37658
|
return ordered;
|
|
36324
37659
|
}
|
|
37660
|
+
const SHARED_WRITE_STEPS = deriveSharedWriteSteps();
|
|
37661
|
+
const sharedWriteMeta = (id) => {
|
|
37662
|
+
const step = SHARED_WRITE_STEPS.get(id);
|
|
37663
|
+
return step ? {
|
|
37664
|
+
writesSharedFile: step.writesSharedFile,
|
|
37665
|
+
dependsOn: step.dependsOn
|
|
37666
|
+
} : {};
|
|
37667
|
+
};
|
|
36325
37668
|
/**
|
|
36326
37669
|
* The static shape of the generation step graph: which steps write which shared
|
|
36327
37670
|
* (read-modify-write) config files, and the `dependsOn` edges that fix a safe order
|
|
36328
|
-
* for those writers.
|
|
36329
|
-
*
|
|
36330
|
-
*
|
|
36331
|
-
*
|
|
36332
|
-
*
|
|
37671
|
+
* for those writers. Both are derived from the processor registry's settable
|
|
37672
|
+
* paths and `SHARED_WRITE_FEATURE_ORDER` (see `shared-file-derive.ts`), so a new
|
|
37673
|
+
* tool or shared path never requires editing this graph. Exported (separately
|
|
37674
|
+
* from the `run` closures, which need a live `config`/`logger`) so
|
|
37675
|
+
* `resolveExecutionOrder`'s ordering guarantee can be tested directly against
|
|
37676
|
+
* the real graph rather than a hand-copied one. Readonly so a consumer can't
|
|
37677
|
+
* mutate this module-level singleton and affect every subsequent `generate()`
|
|
37678
|
+
* call in the process.
|
|
36333
37679
|
*/
|
|
36334
37680
|
const GENERATION_STEP_GRAPH = [
|
|
36335
37681
|
{
|
|
36336
37682
|
id: "ignore",
|
|
36337
|
-
|
|
37683
|
+
...sharedWriteMeta("ignore")
|
|
36338
37684
|
},
|
|
36339
37685
|
{
|
|
36340
37686
|
id: "mcp",
|
|
36341
|
-
|
|
36342
|
-
".amp/settings.json",
|
|
36343
|
-
".augment/settings.json",
|
|
36344
|
-
".codex/config.toml",
|
|
36345
|
-
".config/amp/settings.json",
|
|
36346
|
-
".config/devin/config.json",
|
|
36347
|
-
".config/opencode/opencode.json",
|
|
36348
|
-
".config/zed/settings.json",
|
|
36349
|
-
".devin/config.json",
|
|
36350
|
-
".grok/config.toml",
|
|
36351
|
-
".hermes/config.yaml",
|
|
36352
|
-
".qwen/settings.json",
|
|
36353
|
-
".reasonix/config.toml",
|
|
36354
|
-
".takt/config.yaml",
|
|
36355
|
-
".vibe/config.toml",
|
|
36356
|
-
".zed/settings.json",
|
|
36357
|
-
"kilo.json",
|
|
36358
|
-
"opencode.json",
|
|
36359
|
-
"reasonix.toml"
|
|
36360
|
-
],
|
|
36361
|
-
dependsOn: ["ignore"]
|
|
37687
|
+
...sharedWriteMeta("mcp")
|
|
36362
37688
|
},
|
|
36363
37689
|
{ id: "commands" },
|
|
36364
|
-
{
|
|
37690
|
+
{
|
|
37691
|
+
id: "subagents",
|
|
37692
|
+
...sharedWriteMeta("subagents")
|
|
37693
|
+
},
|
|
36365
37694
|
{ id: "skills" },
|
|
36366
37695
|
{
|
|
36367
37696
|
id: "hooks",
|
|
36368
|
-
|
|
36369
|
-
".augment/settings.json",
|
|
36370
|
-
".claude/settings.json",
|
|
36371
|
-
".codex/config.toml",
|
|
36372
|
-
".config/devin/config.json",
|
|
36373
|
-
".hermes/config.yaml",
|
|
36374
|
-
".kiro/agents/default.json",
|
|
36375
|
-
".qwen/settings.json",
|
|
36376
|
-
".vibe/config.toml"
|
|
36377
|
-
],
|
|
36378
|
-
dependsOn: ["ignore", "mcp"]
|
|
37697
|
+
...sharedWriteMeta("hooks")
|
|
36379
37698
|
},
|
|
36380
37699
|
{
|
|
36381
37700
|
id: "permissions",
|
|
36382
|
-
|
|
36383
|
-
".amp/settings.json",
|
|
36384
|
-
".augment/settings.json",
|
|
36385
|
-
".claude/settings.json",
|
|
36386
|
-
".codex/config.toml",
|
|
36387
|
-
".config/amp/settings.json",
|
|
36388
|
-
".config/devin/config.json",
|
|
36389
|
-
".config/opencode/opencode.json",
|
|
36390
|
-
".config/zed/settings.json",
|
|
36391
|
-
".devin/config.json",
|
|
36392
|
-
".grok/config.toml",
|
|
36393
|
-
".hermes/config.yaml",
|
|
36394
|
-
".kiro/agents/default.json",
|
|
36395
|
-
".qwen/settings.json",
|
|
36396
|
-
".reasonix/config.toml",
|
|
36397
|
-
".takt/config.yaml",
|
|
36398
|
-
".vibe/config.toml",
|
|
36399
|
-
".zed/settings.json",
|
|
36400
|
-
"opencode.json",
|
|
36401
|
-
"reasonix.toml"
|
|
36402
|
-
],
|
|
36403
|
-
dependsOn: [
|
|
36404
|
-
"ignore",
|
|
36405
|
-
"hooks",
|
|
36406
|
-
"mcp"
|
|
36407
|
-
]
|
|
37701
|
+
...sharedWriteMeta("permissions")
|
|
36408
37702
|
},
|
|
36409
37703
|
{
|
|
36410
37704
|
id: "rules",
|
|
36411
|
-
|
|
36412
|
-
dependsOn: [
|
|
36413
|
-
"mcp",
|
|
36414
|
-
"skills",
|
|
36415
|
-
"permissions"
|
|
36416
|
-
]
|
|
37705
|
+
...sharedWriteMeta("rules"),
|
|
37706
|
+
dependsOn: [...sharedWriteMeta("rules").dependsOn ?? [], "skills"]
|
|
36417
37707
|
}
|
|
36418
37708
|
];
|
|
36419
37709
|
/**
|
|
@@ -37105,6 +38395,6 @@ async function importPermissionsCore(params) {
|
|
|
37105
38395
|
return writtenCount;
|
|
37106
38396
|
}
|
|
37107
38397
|
//#endregion
|
|
37108
|
-
export {
|
|
38398
|
+
export { toPosixPath as $, RulesyncCommand as A, createTempDirectory as B, RulesyncHooks as C, RULESYNC_SKILLS_RELATIVE_DIR_PATH as Ct, CLAUDECODE_MEMORIES_DIR_NAME as D, ALL_FEATURES as Dt, CLAUDECODE_LOCAL_RULE_FILE_NAME as E, formatError as Et, ConsoleLogger as F, getFileSize as G, ensureDir as H, JsonLogger as I, listDirectoryFiles as J, getHomeDirectory as K, CLIError as L, stringifyFrontmatter as M, ConfigResolver as N, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as O, ALL_FEATURES_WITH_WILDCARD as Ot, findControlCharacter as P, removeTempDirectory as Q, ErrorCodes as R, HooksProcessor as S, RULESYNC_RULES_RELATIVE_DIR_PATH as St, CLAUDECODE_DIR as T, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Tt, fileExists as U, directoryExists as V, findFilesByGlobs as W, removeDirectory as X, readFileContent as Y, removeFile as Z, RulesyncPermissions as _, RULESYNC_MCP_SCHEMA_URL as _t, convertFromTool as a, RULESYNC_AIIGNORE_FILE_NAME as at, IgnoreProcessor as b, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as bt, RulesyncRuleFrontmatterSchema as c, RULESYNC_CONFIG_RELATIVE_FILE_PATH as ct, RulesyncSubagentFrontmatterSchema as d, RULESYNC_HOOKS_FILE_NAME as dt, writeFileContent as et, SkillsProcessor as f, RULESYNC_HOOKS_RELATIVE_FILE_PATH as ft, SKILL_FILE_NAME as g, RULESYNC_MCP_RELATIVE_FILE_PATH as gt, RulesyncSkillFrontmatterSchema as h, RULESYNC_MCP_FILE_NAME as ht, getProcessorRegistryEntry as i, MAX_FILE_SIZE as it, RulesyncCommandFrontmatterSchema as j, CLAUDECODE_SKILLS_DIR_PATH as k, SubagentsProcessor as l, RULESYNC_CONFIG_SCHEMA_URL as lt, RulesyncSkill as m, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as mt, checkRulesyncDirExists as n, ALL_TOOL_TARGETS_WITH_WILDCARD as nt, RulesProcessor as o, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as ot, getLocalSkillDirNames as p, RULESYNC_IGNORE_RELATIVE_FILE_PATH as pt, isSymlink as q, generate as r, ToolTargetSchema as rt, RulesyncRule as s, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as st, importFromTool as t, ALL_TOOL_TARGETS as tt, RulesyncSubagent as u, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as ut, McpProcessor as v, RULESYNC_OVERVIEW_FILE_NAME as vt, CommandsProcessor as w, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as wt, RulesyncIgnore as x, RULESYNC_RELATIVE_DIR_PATH as xt, RulesyncMcp as y, RULESYNC_PERMISSIONS_FILE_NAME as yt, checkPathTraversal as z };
|
|
37109
38399
|
|
|
37110
|
-
//# sourceMappingURL=import-
|
|
38400
|
+
//# sourceMappingURL=import-BxqZVTKb.js.map
|