rulesync 9.3.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 +4 -61
- package/dist/cli/index.js +2 -59
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-B7VzSVUK.js → import-BxqZVTKb.js} +930 -375
- package/dist/import-BxqZVTKb.js.map +1 -0
- package/dist/{import-CE3Rx3Wt.cjs → import-DVGMvuhx.cjs} +944 -485
- 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-B7VzSVUK.js.map +0 -1
|
@@ -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.
|
|
@@ -6330,42 +6596,6 @@ const CANONICAL_TO_REASONIX_EVENT_NAMES = {
|
|
|
6330
6596
|
*/
|
|
6331
6597
|
const REASONIX_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_REASONIX_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
6332
6598
|
//#endregion
|
|
6333
|
-
//#region src/utils/prototype-pollution.ts
|
|
6334
|
-
/**
|
|
6335
|
-
* Keys that, if walked into when constructing or merging objects from
|
|
6336
|
-
* untrusted input, can mutate `Object.prototype` (or otherwise the prototype
|
|
6337
|
-
* chain) and propagate state to every other object in the runtime. Any code
|
|
6338
|
-
* that copies arbitrary user-supplied keys into a fresh object — frontmatter
|
|
6339
|
-
* parsing, MCP config conversion, settings round-trip — should skip these.
|
|
6340
|
-
*/
|
|
6341
|
-
const PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
|
|
6342
|
-
"__proto__",
|
|
6343
|
-
"constructor",
|
|
6344
|
-
"prototype"
|
|
6345
|
-
]);
|
|
6346
|
-
function isPrototypePollutionKey(key) {
|
|
6347
|
-
return PROTOTYPE_POLLUTION_KEYS.has(key);
|
|
6348
|
-
}
|
|
6349
|
-
/**
|
|
6350
|
-
* Returns a shallow copy of a record's own entries with every
|
|
6351
|
-
* prototype-pollution key (`__proto__`, `constructor`, `prototype`) dropped.
|
|
6352
|
-
*
|
|
6353
|
-
* Use when copying a nested, user-supplied string map — an MCP server's `env`
|
|
6354
|
-
* or `headers` table — into freshly generated config. Carrying such a map by
|
|
6355
|
-
* reference, or re-assigning its keys via bracket notation, would let a literal
|
|
6356
|
-
* `__proto__` key ride along (and re-assigning it would mutate the target's
|
|
6357
|
-
* prototype). Walking the entries through this helper severs that path while
|
|
6358
|
-
* preserving every legitimate key.
|
|
6359
|
-
*/
|
|
6360
|
-
function omitPrototypePollutionKeys(record) {
|
|
6361
|
-
const sanitized = {};
|
|
6362
|
-
for (const [key, value] of Object.entries(record)) {
|
|
6363
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
6364
|
-
sanitized[key] = value;
|
|
6365
|
-
}
|
|
6366
|
-
return sanitized;
|
|
6367
|
-
}
|
|
6368
|
-
//#endregion
|
|
6369
6599
|
//#region src/features/hooks/tool-hooks-converter.ts
|
|
6370
6600
|
function isToolMatcherEntry(x) {
|
|
6371
6601
|
if (x === null || typeof x !== "object") return false;
|
|
@@ -7024,24 +7254,19 @@ var ClaudecodeHooks = class ClaudecodeHooks extends ToolHooks {
|
|
|
7024
7254
|
const paths = ClaudecodeHooks.getSettablePaths({ global });
|
|
7025
7255
|
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
7026
7256
|
const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
|
|
7027
|
-
let settings;
|
|
7028
|
-
try {
|
|
7029
|
-
settings = JSON.parse(existingContent);
|
|
7030
|
-
} catch (error) {
|
|
7031
|
-
throw new Error(`Failed to parse existing Claude settings at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
7032
|
-
}
|
|
7033
7257
|
const config = rulesyncHooks.getJson();
|
|
7034
|
-
const
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
|
|
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
|
|
7039
7269
|
});
|
|
7040
|
-
const merged = {
|
|
7041
|
-
...settings,
|
|
7042
|
-
hooks: claudeHooks
|
|
7043
|
-
};
|
|
7044
|
-
const fileContent = JSON.stringify(merged, null, 2);
|
|
7045
7270
|
return new ClaudecodeHooks({
|
|
7046
7271
|
outputRoot,
|
|
7047
7272
|
relativeDirPath: paths.relativeDirPath,
|
|
@@ -8273,55 +8498,6 @@ var GooseHooks = class GooseHooks extends ToolHooks {
|
|
|
8273
8498
|
}
|
|
8274
8499
|
};
|
|
8275
8500
|
//#endregion
|
|
8276
|
-
//#region src/features/hermes-config.ts
|
|
8277
|
-
function sanitizeHermesConfigValue(value) {
|
|
8278
|
-
if (Array.isArray(value)) return value.map(sanitizeHermesConfigValue);
|
|
8279
|
-
if (!isPlainObject(value)) return value;
|
|
8280
|
-
const sanitized = omitPrototypePollutionKeys(value);
|
|
8281
|
-
const result = {};
|
|
8282
|
-
for (const [key, nestedValue] of Object.entries(sanitized)) {
|
|
8283
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
8284
|
-
result[key] = sanitizeHermesConfigValue(nestedValue);
|
|
8285
|
-
}
|
|
8286
|
-
return result;
|
|
8287
|
-
}
|
|
8288
|
-
function parseHermesConfig(fileContent) {
|
|
8289
|
-
if (!fileContent.trim()) return {};
|
|
8290
|
-
const parsed = load(fileContent);
|
|
8291
|
-
if (isPlainObject(parsed)) return sanitizeHermesConfigValue(parsed);
|
|
8292
|
-
return {};
|
|
8293
|
-
}
|
|
8294
|
-
function stringifyHermesConfig(config) {
|
|
8295
|
-
return dump(config, {
|
|
8296
|
-
noRefs: true,
|
|
8297
|
-
sortKeys: false
|
|
8298
|
-
}).trimEnd() + "\n";
|
|
8299
|
-
}
|
|
8300
|
-
function mergeHermesConfig(fileContent, patch) {
|
|
8301
|
-
return stringifyHermesConfig({
|
|
8302
|
-
...parseHermesConfig(fileContent),
|
|
8303
|
-
...patch
|
|
8304
|
-
});
|
|
8305
|
-
}
|
|
8306
|
-
/**
|
|
8307
|
-
* Recursively merge `patch` into `base` (patch wins). Nested plain objects are
|
|
8308
|
-
* merged key-by-key; every other value (arrays, scalars) is replaced wholesale.
|
|
8309
|
-
* Used to overlay the Hermes-scoped permission override onto the natively
|
|
8310
|
-
* emitted `approvals`/`security` structures without one clobbering the other
|
|
8311
|
-
* (e.g. `approvals.deny` from canonical deny rules coexisting with an
|
|
8312
|
-
* `approvals.mode` from the override). Prototype-pollution keys are dropped.
|
|
8313
|
-
*/
|
|
8314
|
-
function deepMergeHermesConfig(base, patch) {
|
|
8315
|
-
const result = { ...base };
|
|
8316
|
-
for (const [key, patchValue] of Object.entries(patch)) {
|
|
8317
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
8318
|
-
const baseValue = result[key];
|
|
8319
|
-
if (isPlainObject(baseValue) && isPlainObject(patchValue)) result[key] = deepMergeHermesConfig(baseValue, patchValue);
|
|
8320
|
-
else result[key] = sanitizeHermesConfigValue(patchValue);
|
|
8321
|
-
}
|
|
8322
|
-
return result;
|
|
8323
|
-
}
|
|
8324
|
-
//#endregion
|
|
8325
8501
|
//#region src/features/hooks/hermesagent-hooks.ts
|
|
8326
8502
|
/**
|
|
8327
8503
|
* Canonical events that map to a Hermes tool-call event (`pre_tool_call` /
|
|
@@ -8449,10 +8625,21 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
|
|
|
8449
8625
|
return true;
|
|
8450
8626
|
}
|
|
8451
8627
|
setFileContent(fileContent) {
|
|
8452
|
-
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
|
+
});
|
|
8453
8637
|
}
|
|
8454
8638
|
toRulesyncHooks() {
|
|
8455
|
-
const hooks = hermesHooksToCanonical(
|
|
8639
|
+
const hooks = hermesHooksToCanonical(parseSharedConfig({
|
|
8640
|
+
format: "yaml",
|
|
8641
|
+
fileContent: this.getFileContent()
|
|
8642
|
+
}).hooks);
|
|
8456
8643
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
|
|
8457
8644
|
version: 1,
|
|
8458
8645
|
hooks
|
|
@@ -8462,11 +8649,14 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
|
|
|
8462
8649
|
const config = rulesyncHooks.getJson();
|
|
8463
8650
|
return new HermesagentHooks({
|
|
8464
8651
|
outputRoot,
|
|
8465
|
-
fileContent:
|
|
8466
|
-
|
|
8467
|
-
|
|
8468
|
-
|
|
8469
|
-
|
|
8652
|
+
fileContent: stringifySharedConfig({
|
|
8653
|
+
format: "yaml",
|
|
8654
|
+
document: { hooks: canonicalToHermesHooks({
|
|
8655
|
+
config,
|
|
8656
|
+
toolOverrideHooks: config.hermesagent?.hooks,
|
|
8657
|
+
logger
|
|
8658
|
+
}) }
|
|
8659
|
+
})
|
|
8470
8660
|
});
|
|
8471
8661
|
}
|
|
8472
8662
|
};
|
|
@@ -10486,66 +10676,6 @@ var AugmentcodeIgnore = class AugmentcodeIgnore extends ToolIgnore {
|
|
|
10486
10676
|
}
|
|
10487
10677
|
};
|
|
10488
10678
|
//#endregion
|
|
10489
|
-
//#region src/features/claudecode-settings-gateway.ts
|
|
10490
|
-
/**
|
|
10491
|
-
* Single owner of the `.claude/settings.json` `permissions` block, which both
|
|
10492
|
-
* `ignore` (writes `Read(...)` into `permissions.deny`) and `permissions`
|
|
10493
|
-
* (writes the whole `allow`/`ask`/`deny`) read-modify-write. The entry format,
|
|
10494
|
-
* the merge, and the cross-feature ownership rule (permissions' explicit `Read`
|
|
10495
|
-
* rules win over ignore-derived `Read` denies) used to be duplicated across both
|
|
10496
|
-
* feature files; they live here once so each feature just states its intent and
|
|
10497
|
-
* never reasons about the other's existence.
|
|
10498
|
-
*/
|
|
10499
|
-
const READ_TOOL_NAME = "Read";
|
|
10500
|
-
const isReadDenyEntry = (entry) => entry.startsWith(`${READ_TOOL_NAME}(`) && entry.endsWith(")");
|
|
10501
|
-
const buildReadDenyEntry = (pattern) => `${READ_TOOL_NAME}(${pattern})`;
|
|
10502
|
-
const parsePermissionsBlock = (settings) => {
|
|
10503
|
-
const permissions = settings.permissions ?? {};
|
|
10504
|
-
return {
|
|
10505
|
-
allow: permissions.allow ?? [],
|
|
10506
|
-
ask: permissions.ask ?? [],
|
|
10507
|
-
deny: permissions.deny ?? []
|
|
10508
|
-
};
|
|
10509
|
-
};
|
|
10510
|
-
const withPermissions = (settings, next) => {
|
|
10511
|
-
const permissions = { ...settings.permissions };
|
|
10512
|
-
const assign = (key, values) => {
|
|
10513
|
-
if (values.length > 0) permissions[key] = values;
|
|
10514
|
-
else delete permissions[key];
|
|
10515
|
-
};
|
|
10516
|
-
assign("allow", next.allow);
|
|
10517
|
-
assign("ask", next.ask);
|
|
10518
|
-
assign("deny", next.deny);
|
|
10519
|
-
return {
|
|
10520
|
-
...settings,
|
|
10521
|
-
permissions
|
|
10522
|
-
};
|
|
10523
|
-
};
|
|
10524
|
-
const applyIgnoreReadDenies = (params) => {
|
|
10525
|
-
const { settings, readDenies } = params;
|
|
10526
|
-
const current = parsePermissionsBlock(settings);
|
|
10527
|
-
const preservedDeny = current.deny.filter((entry) => !isReadDenyEntry(entry) || readDenies.includes(entry));
|
|
10528
|
-
return withPermissions(settings, {
|
|
10529
|
-
allow: current.allow,
|
|
10530
|
-
ask: current.ask,
|
|
10531
|
-
deny: uniq([...preservedDeny, ...readDenies].toSorted())
|
|
10532
|
-
});
|
|
10533
|
-
};
|
|
10534
|
-
const applyPermissions = (params) => {
|
|
10535
|
-
const { settings, managedToolNames, toolNameOf, allow, ask, deny, logger } = params;
|
|
10536
|
-
const current = parsePermissionsBlock(settings);
|
|
10537
|
-
const keepUnmanaged = (entries) => entries.filter((entry) => !managedToolNames.has(toolNameOf(entry)));
|
|
10538
|
-
if (logger && managedToolNames.has(READ_TOOL_NAME)) {
|
|
10539
|
-
const overwrittenReadDenies = current.deny.filter((entry) => toolNameOf(entry) === READ_TOOL_NAME);
|
|
10540
|
-
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.`);
|
|
10541
|
-
}
|
|
10542
|
-
return withPermissions(settings, {
|
|
10543
|
-
allow: uniq([...keepUnmanaged(current.allow), ...allow].toSorted()),
|
|
10544
|
-
ask: uniq([...keepUnmanaged(current.ask), ...ask].toSorted()),
|
|
10545
|
-
deny: uniq([...keepUnmanaged(current.deny), ...deny].toSorted())
|
|
10546
|
-
});
|
|
10547
|
-
};
|
|
10548
|
-
//#endregion
|
|
10549
10679
|
//#region src/features/ignore/claudecode-ignore.ts
|
|
10550
10680
|
const DEFAULT_FILE_MODE = "shared";
|
|
10551
10681
|
/**
|
|
@@ -11968,7 +12098,6 @@ var AntigravityIdeMcp = class extends AntigravityMcp {
|
|
|
11968
12098
|
};
|
|
11969
12099
|
//#endregion
|
|
11970
12100
|
//#region src/features/mcp/augmentcode-mcp.ts
|
|
11971
|
-
const AUGMENTCODE_GLOBAL_ONLY_MESSAGE = "AugmentCode MCP is global-only; use --global to sync ~/.augment/settings.json";
|
|
11972
12101
|
function parseAugmentcodeSettings(fileContent, relativeDirPath, relativeFilePath) {
|
|
11973
12102
|
const configPath = join(relativeDirPath, relativeFilePath);
|
|
11974
12103
|
let parsed;
|
|
@@ -11983,13 +12112,15 @@ function parseAugmentcodeSettings(fileContent, relativeDirPath, relativeFilePath
|
|
|
11983
12112
|
/**
|
|
11984
12113
|
* AugmentCode (Auggie CLI) MCP servers.
|
|
11985
12114
|
*
|
|
11986
|
-
* MCP servers are persisted in the shared
|
|
11987
|
-
*
|
|
11988
|
-
*
|
|
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
|
|
11989
12118
|
* `toolPermissions`, so generation merges the `mcpServers` block into the
|
|
11990
|
-
* 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).
|
|
11991
12122
|
*
|
|
11992
|
-
* @see https://docs.augmentcode.com/cli/
|
|
12123
|
+
* @see https://docs.augmentcode.com/cli/config
|
|
11993
12124
|
*/
|
|
11994
12125
|
var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
|
|
11995
12126
|
json;
|
|
@@ -12011,9 +12142,14 @@ var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
|
|
|
12011
12142
|
};
|
|
12012
12143
|
}
|
|
12013
12144
|
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
12014
|
-
if (!global) throw new Error(AUGMENTCODE_GLOBAL_ONLY_MESSAGE);
|
|
12015
12145
|
const paths = this.getSettablePaths({ global });
|
|
12016
|
-
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);
|
|
12017
12153
|
const newJson = {
|
|
12018
12154
|
...json,
|
|
12019
12155
|
mcpServers: json.mcpServers ?? {}
|
|
@@ -12028,7 +12164,6 @@ var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
|
|
|
12028
12164
|
});
|
|
12029
12165
|
}
|
|
12030
12166
|
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
|
|
12031
|
-
if (!global) throw new Error(AUGMENTCODE_GLOBAL_ONLY_MESSAGE);
|
|
12032
12167
|
const paths = this.getSettablePaths({ global });
|
|
12033
12168
|
const merged = {
|
|
12034
12169
|
...parseAugmentcodeSettings(await readOrInitializeFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath), JSON.stringify({}, null, 2)), paths.relativeDirPath, paths.relativeFilePath),
|
|
@@ -13678,7 +13813,10 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
|
|
|
13678
13813
|
config;
|
|
13679
13814
|
constructor(params) {
|
|
13680
13815
|
super(params);
|
|
13681
|
-
this.config = this.fileContent !== void 0 ?
|
|
13816
|
+
this.config = this.fileContent !== void 0 ? parseSharedConfig({
|
|
13817
|
+
format: "yaml",
|
|
13818
|
+
fileContent: this.fileContent
|
|
13819
|
+
}) : {};
|
|
13682
13820
|
}
|
|
13683
13821
|
getConfig() {
|
|
13684
13822
|
return this.config;
|
|
@@ -13687,9 +13825,17 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
|
|
|
13687
13825
|
return true;
|
|
13688
13826
|
}
|
|
13689
13827
|
setFileContent(fileContent) {
|
|
13690
|
-
const merged = mergeHermesMcpServers(
|
|
13828
|
+
const merged = mergeHermesMcpServers(parseSharedConfig({
|
|
13829
|
+
format: "yaml",
|
|
13830
|
+
fileContent
|
|
13831
|
+
}), isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
|
|
13691
13832
|
this.config = merged;
|
|
13692
|
-
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
|
+
}));
|
|
13693
13839
|
}
|
|
13694
13840
|
isDeletable() {
|
|
13695
13841
|
return false;
|
|
@@ -13716,12 +13862,21 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
|
|
|
13716
13862
|
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
|
|
13717
13863
|
if (!global) throw new Error(HERMESAGENT_GLOBAL_ONLY_MESSAGE);
|
|
13718
13864
|
const paths = this.getSettablePaths({ global });
|
|
13719
|
-
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()));
|
|
13720
13870
|
return new HermesagentMcp({
|
|
13721
13871
|
outputRoot,
|
|
13722
13872
|
relativeDirPath: paths.relativeDirPath,
|
|
13723
13873
|
relativeFilePath: paths.relativeFilePath,
|
|
13724
|
-
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
|
+
}),
|
|
13725
13880
|
validate,
|
|
13726
13881
|
global
|
|
13727
13882
|
});
|
|
@@ -14886,28 +15041,6 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
|
|
|
14886
15041
|
}
|
|
14887
15042
|
};
|
|
14888
15043
|
//#endregion
|
|
14889
|
-
//#region src/features/shared/takt-config.ts
|
|
14890
|
-
/**
|
|
14891
|
-
* Parse a Takt `config.yaml` into a plain object, treating an empty file as `{}`.
|
|
14892
|
-
*
|
|
14893
|
-
* Shared by the Takt adapters that read-modify-write the same `config.yaml`
|
|
14894
|
-
* (mcp, permissions, ...). Uses `isPlainObject` (not `isRecord`) so class
|
|
14895
|
-
* instances are rejected for prototype-pollution hardening; a YAML mapping
|
|
14896
|
-
* always parses to a plain object.
|
|
14897
|
-
*/
|
|
14898
|
-
function parseTaktConfig(fileContent, relativeDirPath, relativeFilePath) {
|
|
14899
|
-
const configPath = join(relativeDirPath, relativeFilePath);
|
|
14900
|
-
let parsed;
|
|
14901
|
-
try {
|
|
14902
|
-
parsed = fileContent.trim() === "" ? {} : load(fileContent);
|
|
14903
|
-
} catch (error) {
|
|
14904
|
-
throw new Error(`Failed to parse Takt config at ${configPath}: ${formatError(error)}`, { cause: error });
|
|
14905
|
-
}
|
|
14906
|
-
if (parsed === void 0 || parsed === null) return {};
|
|
14907
|
-
if (!isPlainObject(parsed)) throw new Error(`Failed to parse Takt config at ${configPath}: expected a YAML mapping`);
|
|
14908
|
-
return parsed;
|
|
14909
|
-
}
|
|
14910
|
-
//#endregion
|
|
14911
15044
|
//#region src/features/mcp/takt-mcp.ts
|
|
14912
15045
|
/**
|
|
14913
15046
|
* MCP adapter for Takt (`.takt/config.yaml` project / `~/.takt/config.yaml`
|
|
@@ -14973,17 +15106,20 @@ var TaktMcp = class TaktMcp extends ToolMcp {
|
|
|
14973
15106
|
}
|
|
14974
15107
|
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
|
|
14975
15108
|
const paths = TaktMcp.getSettablePaths({ global });
|
|
14976
|
-
const
|
|
15109
|
+
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
15110
|
+
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
14977
15111
|
const allowlist = deriveTransportAllowlist(rulesyncMcp.getMcpServers());
|
|
14978
|
-
const merged = {
|
|
14979
|
-
...config,
|
|
14980
|
-
[TAKT_WORKFLOW_MCP_SERVERS_KEY]: allowlist
|
|
14981
|
-
};
|
|
14982
15112
|
return new TaktMcp({
|
|
14983
15113
|
outputRoot,
|
|
14984
15114
|
relativeDirPath: paths.relativeDirPath,
|
|
14985
15115
|
relativeFilePath: paths.relativeFilePath,
|
|
14986
|
-
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
|
+
}),
|
|
14987
15123
|
validate,
|
|
14988
15124
|
global
|
|
14989
15125
|
});
|
|
@@ -15428,7 +15564,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
15428
15564
|
["augmentcode", {
|
|
15429
15565
|
class: AugmentcodeMcp,
|
|
15430
15566
|
meta: {
|
|
15431
|
-
supportsProject:
|
|
15567
|
+
supportsProject: true,
|
|
15432
15568
|
supportsGlobal: true,
|
|
15433
15569
|
supportsEnabledTools: false,
|
|
15434
15570
|
supportsDisabledTools: false
|
|
@@ -16010,15 +16146,97 @@ const JuniePermissionsOverrideSchema = z.looseObject({
|
|
|
16010
16146
|
defaultBehavior: z.optional(z.string())
|
|
16011
16147
|
});
|
|
16012
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
|
+
/**
|
|
16013
16231
|
* Permissions configuration.
|
|
16014
16232
|
* Keys are tool category names (e.g., "bash", "edit", "read", "webfetch").
|
|
16015
16233
|
* Values are pattern-to-action mappings for that tool category.
|
|
16016
16234
|
*
|
|
16017
16235
|
* The optional `opencode`/`hermes`/`cline`/`kilo`/`claudecode`/`vibe`/`cursor`/
|
|
16018
|
-
* `qwencode`/`reasonix`/`factorydroid`/`warp`/`junie
|
|
16019
|
-
* overrides consumed only by their
|
|
16020
|
-
* `*PermissionsOverrideSchema`); every
|
|
16021
|
-
* block and ignores them.
|
|
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.
|
|
16022
16240
|
*
|
|
16023
16241
|
* @example
|
|
16024
16242
|
* {
|
|
@@ -16039,7 +16257,10 @@ const PermissionsConfigSchema = z.looseObject({
|
|
|
16039
16257
|
reasonix: z.optional(ReasonixPermissionsOverrideSchema),
|
|
16040
16258
|
factorydroid: z.optional(FactorydroidPermissionsOverrideSchema),
|
|
16041
16259
|
warp: z.optional(WarpPermissionsOverrideSchema),
|
|
16042
|
-
junie: z.optional(JuniePermissionsOverrideSchema)
|
|
16260
|
+
junie: z.optional(JuniePermissionsOverrideSchema),
|
|
16261
|
+
takt: z.optional(TaktPermissionsOverrideSchema),
|
|
16262
|
+
amp: z.optional(AmpPermissionsOverrideSchema),
|
|
16263
|
+
"antigravity-cli": z.optional(AntigravityCliPermissionsOverrideSchema)
|
|
16043
16264
|
});
|
|
16044
16265
|
/**
|
|
16045
16266
|
* Full permissions file schema including optional $schema field.
|
|
@@ -16151,6 +16372,39 @@ const AMP_TOOLS_DISABLE_KEY = "amp.tools.disable";
|
|
|
16151
16372
|
* Reference: https://ampcode.com/manual ("amp.permissions").
|
|
16152
16373
|
*/
|
|
16153
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
|
+
}
|
|
16154
16408
|
function parseAmpSettings(fileContent) {
|
|
16155
16409
|
const errors = [];
|
|
16156
16410
|
const parsed = parse(fileContent || "{}", errors, { allowTrailingComma: true });
|
|
@@ -16166,10 +16420,20 @@ function toDisableList(value) {
|
|
|
16166
16420
|
return value.filter((entry) => typeof entry === "string");
|
|
16167
16421
|
}
|
|
16168
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
|
+
/**
|
|
16169
16432
|
* Read an `amp.permissions` array from untrusted parsed settings. Only entries
|
|
16170
|
-
* whose
|
|
16171
|
-
*
|
|
16172
|
-
*
|
|
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.
|
|
16173
16437
|
*/
|
|
16174
16438
|
function toPermissionsList(value) {
|
|
16175
16439
|
if (!Array.isArray(value)) return [];
|
|
@@ -16179,14 +16443,11 @@ function toPermissionsList(value) {
|
|
|
16179
16443
|
const { tool, action } = raw;
|
|
16180
16444
|
if (typeof tool !== "string" || typeof action !== "string") continue;
|
|
16181
16445
|
if (action !== "allow" && action !== "reject" && action !== "ask" && action !== "delegate") continue;
|
|
16182
|
-
const matches = raw.matches;
|
|
16183
|
-
let normalizedMatches;
|
|
16184
|
-
if (isPlainObject(matches) && typeof matches.cmd === "string") normalizedMatches = { cmd: matches.cmd };
|
|
16185
16446
|
entries.push({
|
|
16186
16447
|
...raw,
|
|
16187
16448
|
tool,
|
|
16188
16449
|
action,
|
|
16189
|
-
...
|
|
16450
|
+
...isPlainObject(raw.matches) ? { matches: raw.matches } : {}
|
|
16190
16451
|
});
|
|
16191
16452
|
}
|
|
16192
16453
|
return entries;
|
|
@@ -16211,6 +16472,13 @@ function toPermissionsList(value) {
|
|
|
16211
16472
|
* The settings file is shared with the MCP feature (`amp.mcpServers`), so reads
|
|
16212
16473
|
* and writes merge into the existing JSON rather than overwriting it, and the
|
|
16213
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.
|
|
16214
16482
|
*/
|
|
16215
16483
|
var AmpPermissions = class AmpPermissions extends ToolPermissions {
|
|
16216
16484
|
constructor(params) {
|
|
@@ -16280,15 +16548,25 @@ var AmpPermissions = class AmpPermissions extends ToolPermissions {
|
|
|
16280
16548
|
const jsonDir = join(outputRoot, basePaths.relativeDirPath);
|
|
16281
16549
|
const { fileContent, relativeFilePath } = await this.resolveSettingsFile(jsonDir);
|
|
16282
16550
|
const json = fileContent ? parseAmpSettings(fileContent) : {};
|
|
16283
|
-
const
|
|
16284
|
-
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");
|
|
16285
16556
|
const newJson = {
|
|
16286
16557
|
...json,
|
|
16287
16558
|
[AMP_TOOLS_DISABLE_KEY]: disable
|
|
16288
16559
|
};
|
|
16289
|
-
const mergedPermissions = [
|
|
16560
|
+
const mergedPermissions = mergeAmpPermissions([
|
|
16561
|
+
...permissions,
|
|
16562
|
+
...authoredExtras,
|
|
16563
|
+
...preservedDelegates
|
|
16564
|
+
]);
|
|
16290
16565
|
if (mergedPermissions.length > 0) newJson[AMP_PERMISSIONS_KEY] = mergedPermissions;
|
|
16291
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;
|
|
16292
16570
|
return new AmpPermissions({
|
|
16293
16571
|
outputRoot,
|
|
16294
16572
|
relativeDirPath: basePaths.relativeDirPath,
|
|
@@ -16299,11 +16577,22 @@ var AmpPermissions = class AmpPermissions extends ToolPermissions {
|
|
|
16299
16577
|
}
|
|
16300
16578
|
toRulesyncPermissions() {
|
|
16301
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));
|
|
16302
16583
|
const config = convertAmpToRulesync({
|
|
16303
16584
|
disable: toDisableList(json[AMP_TOOLS_DISABLE_KEY]),
|
|
16304
|
-
permissions:
|
|
16305
|
-
});
|
|
16306
|
-
|
|
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) });
|
|
16307
16596
|
}
|
|
16308
16597
|
validate() {
|
|
16309
16598
|
try {
|
|
@@ -16350,6 +16639,37 @@ const ACTION_PRIORITY = {
|
|
|
16350
16639
|
allow: 2
|
|
16351
16640
|
};
|
|
16352
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
|
+
/**
|
|
16353
16673
|
* Convert a rulesync permissions config into Amp's two permission surfaces.
|
|
16354
16674
|
*
|
|
16355
16675
|
* For each `(category, pattern, action)` — where the category name IS the Amp
|
|
@@ -16414,14 +16734,14 @@ function sortAmpPermissions(entries) {
|
|
|
16414
16734
|
const ap = ACTION_PRIORITY[a.entry.action] ?? 0;
|
|
16415
16735
|
const bp = ACTION_PRIORITY[b.entry.action] ?? 0;
|
|
16416
16736
|
if (ap !== bp) return ap - bp;
|
|
16417
|
-
const aHasCmd = a.entry
|
|
16418
|
-
const bHasCmd = b.entry
|
|
16737
|
+
const aHasCmd = ampMatchesCmd(a.entry) !== void 0 ? 0 : 1;
|
|
16738
|
+
const bHasCmd = ampMatchesCmd(b.entry) !== void 0 ? 0 : 1;
|
|
16419
16739
|
if (aHasCmd !== bHasCmd) return aHasCmd - bHasCmd;
|
|
16420
16740
|
const at = a.entry.tool;
|
|
16421
16741
|
const bt = b.entry.tool;
|
|
16422
16742
|
if (at !== bt) return at < bt ? -1 : 1;
|
|
16423
|
-
const ac = a.entry
|
|
16424
|
-
const bc = b.entry
|
|
16743
|
+
const ac = ampMatchesCmd(a.entry) ?? "";
|
|
16744
|
+
const bc = ampMatchesCmd(b.entry) ?? "";
|
|
16425
16745
|
if (ac !== bc) return ac < bc ? -1 : 1;
|
|
16426
16746
|
return a.index - b.index;
|
|
16427
16747
|
});
|
|
@@ -16460,7 +16780,7 @@ function convertAmpToRulesync({ disable, permissions }) {
|
|
|
16460
16780
|
for (const tool of disable) assign(tool, "*", "deny");
|
|
16461
16781
|
for (const entry of permissions) {
|
|
16462
16782
|
if (entry.action === "delegate") continue;
|
|
16463
|
-
const pattern = entry
|
|
16783
|
+
const pattern = ampMatchesCmd(entry) ?? "*";
|
|
16464
16784
|
const action = entry.action === "reject" ? "deny" : entry.action === "ask" ? "ask" : "allow";
|
|
16465
16785
|
assign(entry.tool, pattern, action);
|
|
16466
16786
|
}
|
|
@@ -16542,6 +16862,11 @@ function buildPermissionEntry$1(toolName, pattern) {
|
|
|
16542
16862
|
* Permissions are written to the global `~/.gemini/antigravity-cli/settings.json`
|
|
16543
16863
|
* file (global scope only). The file holds other CLI settings besides
|
|
16544
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`).
|
|
16545
16870
|
*/
|
|
16546
16871
|
var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPermissions {
|
|
16547
16872
|
constructor(params) {
|
|
@@ -16602,6 +16927,9 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
|
|
|
16602
16927
|
...settings,
|
|
16603
16928
|
permissions: mergedPermissions
|
|
16604
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;
|
|
16605
16933
|
const fileContent = JSON.stringify(merged, null, 2);
|
|
16606
16934
|
return new AntigravityCliPermissions({
|
|
16607
16935
|
outputRoot,
|
|
@@ -16625,7 +16953,12 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
|
|
|
16625
16953
|
ask: permissions.ask ?? [],
|
|
16626
16954
|
deny: permissions.deny ?? []
|
|
16627
16955
|
});
|
|
16628
|
-
|
|
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) });
|
|
16629
16962
|
}
|
|
16630
16963
|
validate() {
|
|
16631
16964
|
return {
|
|
@@ -19311,14 +19644,21 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
|
|
|
19311
19644
|
return true;
|
|
19312
19645
|
}
|
|
19313
19646
|
setFileContent(fileContent) {
|
|
19314
|
-
|
|
19315
|
-
|
|
19316
|
-
|
|
19317
|
-
|
|
19318
|
-
|
|
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
|
+
});
|
|
19319
19656
|
}
|
|
19320
19657
|
toRulesyncPermissions() {
|
|
19321
|
-
const config =
|
|
19658
|
+
const config = parseSharedConfig({
|
|
19659
|
+
format: "yaml",
|
|
19660
|
+
fileContent: this.getFileContent()
|
|
19661
|
+
});
|
|
19322
19662
|
const permissions = config.permissions && typeof config.permissions === "object" ? config.permissions.rulesync : {};
|
|
19323
19663
|
return new RulesyncPermissions({
|
|
19324
19664
|
relativeDirPath: "",
|
|
@@ -19339,11 +19679,17 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
|
|
|
19339
19679
|
enabled: true,
|
|
19340
19680
|
domains: webfetchDeny
|
|
19341
19681
|
} };
|
|
19342
|
-
if (permissions.hermes && typeof permissions.hermes === "object") config =
|
|
19682
|
+
if (permissions.hermes && typeof permissions.hermes === "object") config = mergeSharedConfigDeep({
|
|
19683
|
+
base: config,
|
|
19684
|
+
patch: permissions.hermes
|
|
19685
|
+
});
|
|
19343
19686
|
config.permissions = { rulesync: permissions };
|
|
19344
19687
|
return new HermesagentPermissions({
|
|
19345
19688
|
outputRoot,
|
|
19346
|
-
fileContent:
|
|
19689
|
+
fileContent: stringifySharedConfig({
|
|
19690
|
+
format: "yaml",
|
|
19691
|
+
document: config
|
|
19692
|
+
})
|
|
19347
19693
|
});
|
|
19348
19694
|
}
|
|
19349
19695
|
};
|
|
@@ -20899,6 +21245,8 @@ function isPermissionAction(value) {
|
|
|
20899
21245
|
const TAKT_PROVIDER_KEY = "provider";
|
|
20900
21246
|
const TAKT_PROVIDER_PROFILES_KEY = "provider_profiles";
|
|
20901
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";
|
|
20902
21250
|
const TAKT_DEFAULT_PROVIDER = "claude";
|
|
20903
21251
|
const CATCH_ALL_PATTERN = "*";
|
|
20904
21252
|
/**
|
|
@@ -20928,10 +21276,16 @@ const CATCH_ALL_PATTERN = "*";
|
|
|
20928
21276
|
* `bash: { "*": "deny" }`. These round-trip the generate mapping.
|
|
20929
21277
|
*
|
|
20930
21278
|
* Both project and global scope are supported. The shared config is merged in
|
|
20931
|
-
* place:
|
|
20932
|
-
*
|
|
20933
|
-
*
|
|
20934
|
-
*
|
|
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.
|
|
20935
21289
|
*/
|
|
20936
21290
|
var TaktPermissions = class TaktPermissions extends ToolPermissions {
|
|
20937
21291
|
constructor(params) {
|
|
@@ -20963,37 +21317,62 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
|
|
|
20963
21317
|
}
|
|
20964
21318
|
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false }) {
|
|
20965
21319
|
const paths = TaktPermissions.getSettablePaths({ global });
|
|
20966
|
-
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();
|
|
20967
21329
|
const provider = resolveActiveProvider(config);
|
|
20968
|
-
const mode = deriveTaktPermissionMode(
|
|
20969
|
-
const
|
|
20970
|
-
const
|
|
20971
|
-
const
|
|
20972
|
-
|
|
20973
|
-
[TAKT_PROVIDER_PROFILES_KEY]: {
|
|
20974
|
-
|
|
20975
|
-
[
|
|
20976
|
-
|
|
20977
|
-
|
|
20978
|
-
}
|
|
20979
|
-
}
|
|
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 }
|
|
20980
21340
|
};
|
|
20981
21341
|
return new TaktPermissions({
|
|
20982
21342
|
outputRoot,
|
|
20983
21343
|
relativeDirPath: paths.relativeDirPath,
|
|
20984
21344
|
relativeFilePath: paths.relativeFilePath,
|
|
20985
|
-
fileContent:
|
|
21345
|
+
fileContent: applySharedConfigPatch({
|
|
21346
|
+
fileKey: TAKT_CONFIG_SHARED_FILE_KEY,
|
|
21347
|
+
feature: "permissions",
|
|
21348
|
+
existingContent,
|
|
21349
|
+
patch,
|
|
21350
|
+
filePath
|
|
21351
|
+
}),
|
|
20986
21352
|
validate: true,
|
|
20987
21353
|
global
|
|
20988
21354
|
});
|
|
20989
21355
|
}
|
|
20990
21356
|
toRulesyncPermissions() {
|
|
20991
|
-
const config =
|
|
21357
|
+
const config = parseSharedConfig({
|
|
21358
|
+
format: "yaml",
|
|
21359
|
+
fileContent: this.getFileContent(),
|
|
21360
|
+
filePath: join(this.getRelativeDirPath(), this.getRelativeFilePath()),
|
|
21361
|
+
invalidRootPolicy: "error"
|
|
21362
|
+
});
|
|
20992
21363
|
const provider = resolveActiveProvider(config);
|
|
20993
21364
|
const profiles = isPlainObject(config[TAKT_PROVIDER_PROFILES_KEY]) ? config[TAKT_PROVIDER_PROFILES_KEY] : {};
|
|
20994
|
-
const
|
|
21365
|
+
const profile = isPlainObject(profiles[provider]) ? profiles[provider] : {};
|
|
21366
|
+
const mode = profile[TAKT_DEFAULT_PERMISSION_MODE_KEY];
|
|
20995
21367
|
const rulesyncConfig = taktModeToRulesyncConfig(mode);
|
|
20996
|
-
|
|
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) });
|
|
20997
21376
|
}
|
|
20998
21377
|
validate() {
|
|
20999
21378
|
return {
|
|
@@ -29888,14 +30267,21 @@ def register(ctx):
|
|
|
29888
30267
|
`;
|
|
29889
30268
|
}
|
|
29890
30269
|
function getEnabledPluginConfigContent(currentContent) {
|
|
29891
|
-
const config =
|
|
30270
|
+
const config = parseSharedConfig({
|
|
30271
|
+
format: "yaml",
|
|
30272
|
+
fileContent: currentContent
|
|
30273
|
+
});
|
|
29892
30274
|
const plugins = config.plugins && typeof config.plugins === "object" ? config.plugins : {};
|
|
29893
30275
|
const enabled = Array.isArray(plugins.enabled) ? plugins.enabled : [];
|
|
29894
|
-
|
|
29895
|
-
|
|
29896
|
-
|
|
29897
|
-
|
|
29898
|
-
|
|
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
|
+
});
|
|
29899
30285
|
}
|
|
29900
30286
|
function getSubagentSpec(rulesyncSubagent) {
|
|
29901
30287
|
const json = rulesyncSubagent.getFrontmatter();
|
|
@@ -29981,6 +30367,17 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
|
|
|
29981
30367
|
static getSettablePaths() {
|
|
29982
30368
|
return { relativeDirPath: HERMESAGENT_RULESYNC_SUBAGENTS_DIR_PATH };
|
|
29983
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
|
+
}
|
|
29984
30381
|
static getSettablePathsForRulesyncSubagent(rulesyncSubagent) {
|
|
29985
30382
|
return [join(HERMESAGENT_RULESYNC_SUBAGENTS_DIR_PATH, `${subagentSlug(rulesyncSubagent.getRelativePathFromCwd())}.json`)];
|
|
29986
30383
|
}
|
|
@@ -31719,7 +32116,9 @@ const RulesyncRuleFrontmatterSchema = z.object({
|
|
|
31719
32116
|
})),
|
|
31720
32117
|
kiro: z.optional(z.looseObject({
|
|
31721
32118
|
inclusion: z.optional(z.string()),
|
|
31722
|
-
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())
|
|
31723
32122
|
})),
|
|
31724
32123
|
takt: z.optional(z.looseObject({
|
|
31725
32124
|
name: z.optional(z.string()),
|
|
@@ -34635,7 +35034,8 @@ function toFileMatchPattern(globs) {
|
|
|
34635
35034
|
*
|
|
34636
35035
|
* Precedence:
|
|
34637
35036
|
* 1. An explicit `kiro.inclusion` block round-trips as-is (with `fileMatchPattern`
|
|
34638
|
-
* 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`).
|
|
34639
35039
|
* 2. Otherwise specific (non-wildcard) globs map to `fileMatch`, scoping the rule
|
|
34640
35040
|
* to matching files instead of leaving it implicitly always-on.
|
|
34641
35041
|
* 3. Otherwise the rule stays `always` — represented by omitting the block so the
|
|
@@ -34653,6 +35053,11 @@ function deriveKiroInclusion({ kiro, globs }) {
|
|
|
34653
35053
|
fileMatchPattern
|
|
34654
35054
|
} : { inclusion: "fileMatch" };
|
|
34655
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
|
+
};
|
|
34656
35061
|
return { inclusion: kiro.inclusion };
|
|
34657
35062
|
}
|
|
34658
35063
|
const fileMatchPattern = toFileMatchPattern(specificGlobs);
|
|
@@ -34724,6 +35129,8 @@ var KiroRule = class KiroRule extends ToolRule {
|
|
|
34724
35129
|
const rawPattern = frontmatter.fileMatchPattern;
|
|
34725
35130
|
const fileMatchPattern = Array.isArray(rawPattern) ? rawPattern.filter((p) => typeof p === "string") : typeof rawPattern === "string" ? rawPattern : void 0;
|
|
34726
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;
|
|
34727
35134
|
return new RulesyncRule({
|
|
34728
35135
|
outputRoot: process.cwd(),
|
|
34729
35136
|
relativeDirPath: RulesyncRule.getSettablePaths().recommended.relativeDirPath,
|
|
@@ -34734,7 +35141,9 @@ var KiroRule = class KiroRule extends ToolRule {
|
|
|
34734
35141
|
globs,
|
|
34735
35142
|
kiro: {
|
|
34736
35143
|
inclusion,
|
|
34737
|
-
...fileMatchPattern !== void 0 ? { fileMatchPattern } : {}
|
|
35144
|
+
...fileMatchPattern !== void 0 ? { fileMatchPattern } : {},
|
|
35145
|
+
...inclusion === "auto" && name !== void 0 ? { name } : {},
|
|
35146
|
+
...inclusion === "auto" && description !== void 0 ? { description } : {}
|
|
34738
35147
|
}
|
|
34739
35148
|
},
|
|
34740
35149
|
body
|
|
@@ -36908,6 +37317,197 @@ function buildPermissionsStrategy(ctx) {
|
|
|
36908
37317
|
};
|
|
36909
37318
|
}
|
|
36910
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
|
|
36911
37511
|
//#region src/lib/generate.ts
|
|
36912
37512
|
async function processFeatureGeneration(params) {
|
|
36913
37513
|
const { config, processor, toolFiles, skipFilePaths } = params;
|
|
@@ -37057,98 +37657,53 @@ function resolveExecutionOrder(steps) {
|
|
|
37057
37657
|
if (ordered.length !== steps.length) throw new Error("Generation steps contain a cyclic 'dependsOn' dependency.");
|
|
37058
37658
|
return ordered;
|
|
37059
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
|
+
};
|
|
37060
37668
|
/**
|
|
37061
37669
|
* The static shape of the generation step graph: which steps write which shared
|
|
37062
37670
|
* (read-modify-write) config files, and the `dependsOn` edges that fix a safe order
|
|
37063
|
-
* for those writers.
|
|
37064
|
-
*
|
|
37065
|
-
*
|
|
37066
|
-
*
|
|
37067
|
-
*
|
|
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.
|
|
37068
37679
|
*/
|
|
37069
37680
|
const GENERATION_STEP_GRAPH = [
|
|
37070
37681
|
{
|
|
37071
37682
|
id: "ignore",
|
|
37072
|
-
|
|
37683
|
+
...sharedWriteMeta("ignore")
|
|
37073
37684
|
},
|
|
37074
37685
|
{
|
|
37075
37686
|
id: "mcp",
|
|
37076
|
-
|
|
37077
|
-
".amp/settings.json",
|
|
37078
|
-
".augment/settings.json",
|
|
37079
|
-
".codex/config.toml",
|
|
37080
|
-
".config/amp/settings.json",
|
|
37081
|
-
".config/devin/config.json",
|
|
37082
|
-
".config/opencode/opencode.json",
|
|
37083
|
-
".config/zed/settings.json",
|
|
37084
|
-
".devin/config.json",
|
|
37085
|
-
".grok/config.toml",
|
|
37086
|
-
".hermes/config.yaml",
|
|
37087
|
-
".qwen/settings.json",
|
|
37088
|
-
".reasonix/config.toml",
|
|
37089
|
-
".takt/config.yaml",
|
|
37090
|
-
".vibe/config.toml",
|
|
37091
|
-
".zed/settings.json",
|
|
37092
|
-
"kilo.json",
|
|
37093
|
-
"opencode.json",
|
|
37094
|
-
"reasonix.toml"
|
|
37095
|
-
],
|
|
37096
|
-
dependsOn: ["ignore"]
|
|
37687
|
+
...sharedWriteMeta("mcp")
|
|
37097
37688
|
},
|
|
37098
37689
|
{ id: "commands" },
|
|
37099
|
-
{
|
|
37690
|
+
{
|
|
37691
|
+
id: "subagents",
|
|
37692
|
+
...sharedWriteMeta("subagents")
|
|
37693
|
+
},
|
|
37100
37694
|
{ id: "skills" },
|
|
37101
37695
|
{
|
|
37102
37696
|
id: "hooks",
|
|
37103
|
-
|
|
37104
|
-
".augment/settings.json",
|
|
37105
|
-
".claude/settings.json",
|
|
37106
|
-
".codex/config.toml",
|
|
37107
|
-
".config/devin/config.json",
|
|
37108
|
-
".hermes/config.yaml",
|
|
37109
|
-
".kiro/agents/default.json",
|
|
37110
|
-
".qwen/settings.json",
|
|
37111
|
-
".vibe/config.toml"
|
|
37112
|
-
],
|
|
37113
|
-
dependsOn: ["ignore", "mcp"]
|
|
37697
|
+
...sharedWriteMeta("hooks")
|
|
37114
37698
|
},
|
|
37115
37699
|
{
|
|
37116
37700
|
id: "permissions",
|
|
37117
|
-
|
|
37118
|
-
".amp/settings.json",
|
|
37119
|
-
".augment/settings.json",
|
|
37120
|
-
".claude/settings.json",
|
|
37121
|
-
".codex/config.toml",
|
|
37122
|
-
".config/amp/settings.json",
|
|
37123
|
-
".config/devin/config.json",
|
|
37124
|
-
".config/opencode/opencode.json",
|
|
37125
|
-
".config/zed/settings.json",
|
|
37126
|
-
".devin/config.json",
|
|
37127
|
-
".grok/config.toml",
|
|
37128
|
-
".hermes/config.yaml",
|
|
37129
|
-
".kiro/agents/default.json",
|
|
37130
|
-
".qwen/settings.json",
|
|
37131
|
-
".reasonix/config.toml",
|
|
37132
|
-
".takt/config.yaml",
|
|
37133
|
-
".vibe/config.toml",
|
|
37134
|
-
".zed/settings.json",
|
|
37135
|
-
"opencode.json",
|
|
37136
|
-
"reasonix.toml"
|
|
37137
|
-
],
|
|
37138
|
-
dependsOn: [
|
|
37139
|
-
"ignore",
|
|
37140
|
-
"hooks",
|
|
37141
|
-
"mcp"
|
|
37142
|
-
]
|
|
37701
|
+
...sharedWriteMeta("permissions")
|
|
37143
37702
|
},
|
|
37144
37703
|
{
|
|
37145
37704
|
id: "rules",
|
|
37146
|
-
|
|
37147
|
-
dependsOn: [
|
|
37148
|
-
"mcp",
|
|
37149
|
-
"skills",
|
|
37150
|
-
"permissions"
|
|
37151
|
-
]
|
|
37705
|
+
...sharedWriteMeta("rules"),
|
|
37706
|
+
dependsOn: [...sharedWriteMeta("rules").dependsOn ?? [], "skills"]
|
|
37152
37707
|
}
|
|
37153
37708
|
];
|
|
37154
37709
|
/**
|
|
@@ -37840,6 +38395,6 @@ async function importPermissionsCore(params) {
|
|
|
37840
38395
|
return writtenCount;
|
|
37841
38396
|
}
|
|
37842
38397
|
//#endregion
|
|
37843
|
-
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 };
|
|
37844
38399
|
|
|
37845
|
-
//# sourceMappingURL=import-
|
|
38400
|
+
//# sourceMappingURL=import-BxqZVTKb.js.map
|