rulesync 37.1.0 → 38.0.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/README.md +3 -3
- package/dist/cli/index.cjs +5 -5
- package/dist/cli/index.js +5 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-DYYG88Rs.cjs → import-B9NOtObR.cjs} +2563 -336
- package/dist/{import-BsEFYJsg.js → import-CoFTCNq7.js} +2548 -321
- package/dist/{import-BsEFYJsg.js.map → import-CoFTCNq7.js.map} +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +11 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -457,36 +457,36 @@ const isFeatureValueEnabled = (value) => {
|
|
|
457
457
|
const parseCommaSeparatedList = (value) => value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
458
458
|
//#endregion
|
|
459
459
|
//#region src/constants/rulesync-paths.ts
|
|
460
|
-
const { join: join$
|
|
460
|
+
const { join: join$426 } = node_path.posix;
|
|
461
461
|
const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
|
|
462
462
|
const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
|
|
463
463
|
const RULESYNC_USER_CONFIG_DIR_NAME = "rulesync";
|
|
464
464
|
const RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME = ".config";
|
|
465
465
|
const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
|
|
466
466
|
const RULES_FEATURE_SUBDIR = "rules";
|
|
467
|
-
const CURATED_RULES_FEATURE_SUBDIR = join$
|
|
467
|
+
const CURATED_RULES_FEATURE_SUBDIR = join$426(RULES_FEATURE_SUBDIR, ".curated");
|
|
468
468
|
const COMMANDS_FEATURE_SUBDIR = "commands";
|
|
469
469
|
const SUBAGENTS_FEATURE_SUBDIR = "subagents";
|
|
470
470
|
const CHECKS_FEATURE_SUBDIR = "checks";
|
|
471
471
|
const SKILLS_FEATURE_SUBDIR = "skills";
|
|
472
|
-
const CURATED_SKILLS_FEATURE_SUBDIR = join$
|
|
473
|
-
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$
|
|
474
|
-
const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$
|
|
475
|
-
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$
|
|
476
|
-
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$
|
|
477
|
-
const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$
|
|
478
|
-
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$
|
|
479
|
-
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$
|
|
480
|
-
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$
|
|
481
|
-
join$
|
|
482
|
-
const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$
|
|
483
|
-
const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$
|
|
472
|
+
const CURATED_SKILLS_FEATURE_SUBDIR = join$426(SKILLS_FEATURE_SUBDIR, ".curated");
|
|
473
|
+
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
|
|
474
|
+
const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
|
|
475
|
+
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
|
|
476
|
+
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
|
|
477
|
+
const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
|
|
478
|
+
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
|
|
479
|
+
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
|
|
480
|
+
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
|
|
481
|
+
join$426(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
|
|
482
|
+
const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
|
|
483
|
+
const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
|
|
484
484
|
const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
|
|
485
|
-
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$
|
|
485
|
+
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
|
|
486
486
|
const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
|
|
487
487
|
const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
|
|
488
|
-
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$
|
|
489
|
-
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$
|
|
488
|
+
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
|
|
489
|
+
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
|
|
490
490
|
const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
|
|
491
491
|
const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
|
|
492
492
|
const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
|
|
@@ -652,6 +652,7 @@ const mcpProcessorToolTargetTuple = [
|
|
|
652
652
|
"musecode",
|
|
653
653
|
"mimocode",
|
|
654
654
|
"omp",
|
|
655
|
+
"openclaw",
|
|
655
656
|
"opencode",
|
|
656
657
|
"pi",
|
|
657
658
|
"pool",
|
|
@@ -704,6 +705,7 @@ const commandsProcessorToolTargetTuple = [
|
|
|
704
705
|
"kiro",
|
|
705
706
|
"kiro-cli",
|
|
706
707
|
"kiro-ide",
|
|
708
|
+
"lettacode",
|
|
707
709
|
"mimocode",
|
|
708
710
|
"omp",
|
|
709
711
|
"opencode",
|
|
@@ -931,6 +933,7 @@ const permissionsProcessorToolTargetTuple = [
|
|
|
931
933
|
"rovodev",
|
|
932
934
|
"tabnine",
|
|
933
935
|
"takt",
|
|
936
|
+
"trae",
|
|
934
937
|
"vibe",
|
|
935
938
|
"warp",
|
|
936
939
|
"warpcli",
|
|
@@ -4764,10 +4767,13 @@ const CLAUDE_HOOK_EVENTS = [
|
|
|
4764
4767
|
* eight events: `PreToolUse`, `PostToolUse`, `PermissionRequest`,
|
|
4765
4768
|
* `UserPromptSubmit`, `Stop`, `SessionStart`, `SessionEnd`, and
|
|
4766
4769
|
* `PostCompaction` (fires after context compaction; added in the Devin CLI
|
|
4767
|
-
* stable changelog — https://docs.devin.ai/cli/changelog/stable).
|
|
4768
|
-
*
|
|
4769
|
-
*
|
|
4770
|
-
*
|
|
4770
|
+
* stable changelog — https://docs.devin.ai/cli/changelog/stable). CLI
|
|
4771
|
+
* v3000.11.92 added `PreCompact` and `PostCompact`, which run before and after
|
|
4772
|
+
* compaction with `manual` / `auto` trigger matchers; `PostCompaction` stays
|
|
4773
|
+
* supported. The tool/permission events (`PreToolUse`/`PostToolUse`/
|
|
4774
|
+
* `PermissionRequest`) carry a `matcher` (regex against `tool_name`) and
|
|
4775
|
+
* `PreCompact` a trigger matcher; the other session/turn/compaction events do
|
|
4776
|
+
* not.
|
|
4771
4777
|
*
|
|
4772
4778
|
* Hooks live in `.devin/hooks.v1.json` (project, standalone — the hooks object
|
|
4773
4779
|
* is the entire file) or under the `"hooks"` key of `.devin/config.json` /
|
|
@@ -4783,6 +4789,7 @@ const DEVIN_HOOK_EVENTS = [
|
|
|
4783
4789
|
"beforeSubmitPrompt",
|
|
4784
4790
|
"stop",
|
|
4785
4791
|
"permissionRequest",
|
|
4792
|
+
"preCompact",
|
|
4786
4793
|
"postCompact"
|
|
4787
4794
|
];
|
|
4788
4795
|
/**
|
|
@@ -6166,12 +6173,17 @@ const CANONICAL_TO_DEVIN_EVENT_NAMES = {
|
|
|
6166
6173
|
beforeSubmitPrompt: "UserPromptSubmit",
|
|
6167
6174
|
stop: "Stop",
|
|
6168
6175
|
permissionRequest: "PermissionRequest",
|
|
6176
|
+
preCompact: "PreCompact",
|
|
6169
6177
|
postCompact: "PostCompaction"
|
|
6170
6178
|
};
|
|
6171
6179
|
/**
|
|
6172
|
-
* Map Devin Local PascalCase event names to canonical camelCase.
|
|
6180
|
+
* Map Devin Local PascalCase event names to canonical camelCase. `PostCompact`
|
|
6181
|
+
* (CLI v3000.11.92) is accepted on import as an alias of `PostCompaction`.
|
|
6173
6182
|
*/
|
|
6174
|
-
const DEVIN_TO_CANONICAL_EVENT_NAMES =
|
|
6183
|
+
const DEVIN_TO_CANONICAL_EVENT_NAMES = {
|
|
6184
|
+
...Object.fromEntries(Object.entries(CANONICAL_TO_DEVIN_EVENT_NAMES).map(([k, v]) => [v, k])),
|
|
6185
|
+
PostCompact: "postCompact"
|
|
6186
|
+
};
|
|
6175
6187
|
/**
|
|
6176
6188
|
* Map canonical camelCase event names to AugmentCode PascalCase.
|
|
6177
6189
|
* Auggie reuses the same PascalCase names as Claude for the events it supports.
|
|
@@ -7104,6 +7116,17 @@ const CodexcliMcpToolConfigSchema = zod_mini.z.looseObject({
|
|
|
7104
7116
|
function isCodexcliToolConfig(value) {
|
|
7105
7117
|
return CodexcliMcpToolConfigSchema.safeParse(value).success;
|
|
7106
7118
|
}
|
|
7119
|
+
const CrushChannelReplyRouteSchema = zod_mini.z.looseObject({
|
|
7120
|
+
tool: zod_mini.z.string(),
|
|
7121
|
+
target_param: zod_mini.z.optional(zod_mini.z.string()),
|
|
7122
|
+
target_meta: zod_mini.z.optional(zod_mini.z.string())
|
|
7123
|
+
});
|
|
7124
|
+
const CrushChannelReplySchema = zod_mini.z.looseObject({
|
|
7125
|
+
user: zod_mini.z.optional(CrushChannelReplyRouteSchema),
|
|
7126
|
+
group: zod_mini.z.optional(CrushChannelReplyRouteSchema),
|
|
7127
|
+
message_param: zod_mini.z.optional(zod_mini.z.string()),
|
|
7128
|
+
suppress_tools: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
|
|
7129
|
+
});
|
|
7107
7130
|
const McpServerSchema = zod_mini.z.looseObject({
|
|
7108
7131
|
type: zod_mini.z.optional(zod_mini.z.enum([
|
|
7109
7132
|
"local",
|
|
@@ -7145,6 +7168,9 @@ const McpServerSchema = zod_mini.z.looseObject({
|
|
|
7145
7168
|
crushOauthClientSecret: zod_mini.z.optional(zod_mini.z.string()),
|
|
7146
7169
|
crushOauthCallbackPort: zod_mini.z.optional(zod_mini.z.int()),
|
|
7147
7170
|
crushSessionless: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
7171
|
+
crushChannelEnabled: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
7172
|
+
crushChannelReply: zod_mini.z.optional(CrushChannelReplySchema),
|
|
7173
|
+
zcodeOauth: zod_mini.z.optional(zod_mini.z.looseObject({ type: zod_mini.z.string() })),
|
|
7148
7174
|
poolAllow: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
7149
7175
|
headers: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
|
|
7150
7176
|
/**
|
|
@@ -7256,6 +7282,7 @@ const RulesyncMcpFileSchema = zod_mini.z.looseObject({
|
|
|
7256
7282
|
kiro: zod_mini.z.optional(toolScopedMcpSchema),
|
|
7257
7283
|
mimocode: zod_mini.z.optional(toolScopedMcpSchema),
|
|
7258
7284
|
omp: zod_mini.z.optional(toolScopedMcpSchema),
|
|
7285
|
+
openclaw: zod_mini.z.optional(toolScopedMcpSchema),
|
|
7259
7286
|
opencode: zod_mini.z.optional(toolScopedMcpSchema),
|
|
7260
7287
|
pi: zod_mini.z.optional(toolScopedMcpSchema),
|
|
7261
7288
|
qoder: zod_mini.z.optional(toolScopedMcpSchema),
|
|
@@ -7603,11 +7630,16 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
|
|
|
7603
7630
|
"crushOauthClientSecret",
|
|
7604
7631
|
"crushOauthCallbackPort",
|
|
7605
7632
|
"crushSessionless",
|
|
7633
|
+
"crushChannelEnabled",
|
|
7634
|
+
"crushChannelReply",
|
|
7606
7635
|
"poolAllow",
|
|
7636
|
+
"zcodeOauth",
|
|
7607
7637
|
"oauth_client_id",
|
|
7608
7638
|
"oauth_client_secret",
|
|
7609
7639
|
"oauth_callback_port",
|
|
7610
7640
|
"sessionless",
|
|
7641
|
+
"channel_enabled",
|
|
7642
|
+
"channel_reply",
|
|
7611
7643
|
"enabled"
|
|
7612
7644
|
])];
|
|
7613
7645
|
}));
|
|
@@ -8343,8 +8375,12 @@ const TabninePermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
|
8343
8375
|
* are merged into the matching `reasonix.toml` table and emitted only for
|
|
8344
8376
|
* Reasonix, while the shared `permission` block continues to drive
|
|
8345
8377
|
* `[permissions].allow`/`ask`/`deny`. Kept `looseObject` (verbatim passthrough).
|
|
8346
|
-
*
|
|
8347
|
-
*
|
|
8378
|
+
* The user-only `[sandbox]` keys (`allow_devices`, `egress_unlisted`,
|
|
8379
|
+
* `host_authorities`) are written only by `--global` generate: Reasonix replaces
|
|
8380
|
+
* a project value with the user's (`narrowSandbox` in `project_scope.go`).
|
|
8381
|
+
* Note: the whole `[sandbox]` table round-trips (minus the user-only keys of a
|
|
8382
|
+
* project file), but only the plan-mode keys are re-extracted from `[agent]` on
|
|
8383
|
+
* import — other `agent` keys authored here reach
|
|
8348
8384
|
* `reasonix.toml` on generate but are not re-extracted back into the override.
|
|
8349
8385
|
*
|
|
8350
8386
|
* @example
|
|
@@ -8376,14 +8412,52 @@ const ReasonixPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
|
8376
8412
|
* output-style` rule option) or a `plugin-name:style-name` reference. The
|
|
8377
8413
|
* shared `permission` block continues to drive `permissions.allow`/`ask`/`deny`.
|
|
8378
8414
|
*
|
|
8415
|
+
* The other documented settings with no canonical category are authored here
|
|
8416
|
+
* too and merged into the same file, key by key, over what the file already
|
|
8417
|
+
* holds: `general.defaultPermissionMode`, `tools.core` / `tools.exclude`, the
|
|
8418
|
+
* MCP approval and loading keys under `mcp` (`allowed`, `excluded`,
|
|
8419
|
+
* `enableAllProjectMcpServers`, `enabledProjectMcpServers`, `lazyLoad`),
|
|
8420
|
+
* `agents.overrides`, the top-level `agentsMdExcludes` and the top-level
|
|
8421
|
+
* `autoMode` classifier guidance (`allow` / `soft_deny` / `environment`).
|
|
8422
|
+
* Qoder reads `autoMode` from the user settings only, so it is written by
|
|
8423
|
+
* `--global` generate alone. Only the keys listed here are written; any other
|
|
8424
|
+
* key of these groups or of `autoMode` (`tools.shell.pager`,
|
|
8425
|
+
* `general.preferredEditor`, ...) is skipped with a warning. Each authored key
|
|
8426
|
+
* is laid over what the file holds; removing it from the override leaves the
|
|
8427
|
+
* previously written value in place.
|
|
8428
|
+
*
|
|
8379
8429
|
* @see https://docs.qoder.com/cli/output-styles
|
|
8380
8430
|
* @see https://docs.qoder.com/cli/settings-reference
|
|
8431
|
+
* @see https://docs.qoder.com/en/cli/permissions
|
|
8381
8432
|
* @example
|
|
8382
8433
|
* { "outputStyle": "concise-review" }
|
|
8434
|
+
* @example
|
|
8435
|
+
* { "mcp": { "enabledProjectMcpServers": ["playwright"], "lazyLoad": true },
|
|
8436
|
+
* "tools": { "exclude": ["WebSearch"] },
|
|
8437
|
+
* "autoMode": { "environment": ["This is a pnpm monorepo"] } }
|
|
8383
8438
|
*/
|
|
8384
8439
|
const QoderPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
8385
8440
|
permission: zod_mini.z.optional(ToolScopedPermissionSchema),
|
|
8386
|
-
outputStyle: zod_mini.z.optional(zod_mini.z.string().check(zod_mini.z.trim(), zod_mini.z.minLength(1)))
|
|
8441
|
+
outputStyle: zod_mini.z.optional(zod_mini.z.string().check(zod_mini.z.trim(), zod_mini.z.minLength(1))),
|
|
8442
|
+
general: zod_mini.z.optional(zod_mini.z.looseObject({ defaultPermissionMode: zod_mini.z.optional(zod_mini.z.string().check(zod_mini.z.trim(), zod_mini.z.minLength(1))) })),
|
|
8443
|
+
tools: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
8444
|
+
core: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
8445
|
+
exclude: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
|
|
8446
|
+
})),
|
|
8447
|
+
mcp: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
8448
|
+
allowed: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
8449
|
+
excluded: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
8450
|
+
enableAllProjectMcpServers: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
8451
|
+
enabledProjectMcpServers: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
8452
|
+
lazyLoad: zod_mini.z.optional(zod_mini.z.boolean())
|
|
8453
|
+
})),
|
|
8454
|
+
agents: zod_mini.z.optional(zod_mini.z.looseObject({ overrides: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.looseObject({}))) })),
|
|
8455
|
+
autoMode: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
8456
|
+
allow: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
8457
|
+
soft_deny: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
8458
|
+
environment: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
|
|
8459
|
+
})),
|
|
8460
|
+
agentsMdExcludes: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
|
|
8387
8461
|
});
|
|
8388
8462
|
/**
|
|
8389
8463
|
* One native Factory Droid permission rule. `match.prefix` positions are either
|
|
@@ -9171,14 +9245,40 @@ const ZedPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
|
9171
9245
|
*
|
|
9172
9246
|
* @example
|
|
9173
9247
|
* { "sandbox": { "allowed_domains": ["github.com"], "network_mode": "limited" } }
|
|
9248
|
+
* The same override carries the repo-level plugin governance lists Devin reads
|
|
9249
|
+
* from a checkout's `.devin/config.json`: `requiredPlugins` (plugin sources
|
|
9250
|
+
* installed automatically), `optionalPlugins` (endorsed sources that carve an
|
|
9251
|
+
* exception out of a forbidden entry; never installed) and `forbiddenPlugins`
|
|
9252
|
+
* (plugin identities or `*` globs that may not load). A source is a string
|
|
9253
|
+
* (`"owner/repo"` or a git URL) or an object such as
|
|
9254
|
+
* `{ "source": "github", "repo": "owner/repo" }`. They are a repo governance
|
|
9255
|
+
* level, so they are emitted at project scope only.
|
|
9256
|
+
*
|
|
9174
9257
|
* @example
|
|
9175
9258
|
* { "sandbox": { "excluded": { "allow": ["Exec(git status *)"], "deny": ["Exec(git tag *)"] } } }
|
|
9259
|
+
* @example
|
|
9260
|
+
* { "forbiddenPlugins": ["*"], "optionalPlugins": ["acme/approved"] }
|
|
9176
9261
|
* @see https://docs.devin.ai/cli/sandbox
|
|
9177
9262
|
* @see https://docs.devin.ai/cli/reference/configuration/config-file
|
|
9263
|
+
* @see https://docs.devin.ai/cli/extensibility/plugins/overview
|
|
9264
|
+
*/
|
|
9265
|
+
const DevinPluginSourceListSchema = zod_mini.z.array(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})]));
|
|
9266
|
+
/**
|
|
9267
|
+
* Shapes of the Devin repo-level plugin governance lists, keyed by their
|
|
9268
|
+
* `.devin/config.json` name. Shared by the override schema and the import,
|
|
9269
|
+
* which drops a list that would not pass it.
|
|
9178
9270
|
*/
|
|
9271
|
+
const DEVIN_PLUGIN_GOVERNANCE_SCHEMAS = {
|
|
9272
|
+
requiredPlugins: DevinPluginSourceListSchema,
|
|
9273
|
+
optionalPlugins: DevinPluginSourceListSchema,
|
|
9274
|
+
forbiddenPlugins: zod_mini.z.array(zod_mini.z.string())
|
|
9275
|
+
};
|
|
9179
9276
|
const DevinPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
9180
9277
|
permission: zod_mini.z.optional(ToolScopedPermissionSchema),
|
|
9181
|
-
sandbox: zod_mini.z.optional(zod_mini.z.looseObject({}))
|
|
9278
|
+
sandbox: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
9279
|
+
requiredPlugins: zod_mini.z.optional(DEVIN_PLUGIN_GOVERNANCE_SCHEMAS.requiredPlugins),
|
|
9280
|
+
optionalPlugins: zod_mini.z.optional(DEVIN_PLUGIN_GOVERNANCE_SCHEMAS.optionalPlugins),
|
|
9281
|
+
forbiddenPlugins: zod_mini.z.optional(DEVIN_PLUGIN_GOVERNANCE_SCHEMAS.forbiddenPlugins)
|
|
9182
9282
|
});
|
|
9183
9283
|
/**
|
|
9184
9284
|
* Permissions configuration.
|
|
@@ -9252,6 +9352,7 @@ const PermissionsConfigSchema = zod_mini.z.looseObject({
|
|
|
9252
9352
|
"kimi-code": zod_mini.z.optional(KimiCodePermissionsOverrideSchema),
|
|
9253
9353
|
roo: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
9254
9354
|
rovodev: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
9355
|
+
trae: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
9255
9356
|
zoocode: zod_mini.z.optional(CanonicalPermissionsOverrideSchema)
|
|
9256
9357
|
});
|
|
9257
9358
|
/**
|
|
@@ -11110,6 +11211,11 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
11110
11211
|
compatibility: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})])),
|
|
11111
11212
|
metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
|
|
11112
11213
|
})),
|
|
11214
|
+
musecode: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
11215
|
+
license: zod_mini.z.optional(zod_mini.z.string()),
|
|
11216
|
+
compatibility: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})])),
|
|
11217
|
+
metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
|
|
11218
|
+
})),
|
|
11113
11219
|
zcode: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
11114
11220
|
when_to_use: zod_mini.z.optional(zod_mini.z.string()),
|
|
11115
11221
|
license: zod_mini.z.optional(zod_mini.z.string()),
|
|
@@ -11540,8 +11646,20 @@ const GITLABDUO_MR_REVIEW_INSTRUCTIONS_FILE_NAME = "mr-review-instructions.yaml"
|
|
|
11540
11646
|
const GITLABDUO_COMMANDS_DIR = (0, node_path.join)(".agents", "commands");
|
|
11541
11647
|
/** User custom slash commands. */
|
|
11542
11648
|
const GITLABDUO_GLOBAL_COMMANDS_DIR = (0, node_path.join)(GITLABDUO_DIR, "commands");
|
|
11543
|
-
/**
|
|
11544
|
-
|
|
11649
|
+
/**
|
|
11650
|
+
* Project skills live in the cross-tool `.agents/skills/` directory, which
|
|
11651
|
+
* GitLab Duo reads alongside a non-hidden `skills/` directory at the project
|
|
11652
|
+
* root; when both hold a skill of the same name, `.agents/skills/` wins.
|
|
11653
|
+
*
|
|
11654
|
+
* @see https://docs.gitlab.com/user/duo_agent_platform/customize/agent_skills/
|
|
11655
|
+
*/
|
|
11656
|
+
const GITLABDUO_SKILLS_DIR = (0, node_path.join)(".agents", "skills");
|
|
11657
|
+
/**
|
|
11658
|
+
* The project-root `skills/` directory GitLab Duo also reads. Earlier rulesync
|
|
11659
|
+
* versions wrote project skills there; it is now an import-only root, since it
|
|
11660
|
+
* routinely holds hand-authored skills rulesync must not manage.
|
|
11661
|
+
*/
|
|
11662
|
+
const GITLABDUO_LEGACY_SKILLS_DIR = "skills";
|
|
11545
11663
|
/** User skills (requires `--enable-global-skills` / `GITLAB_ENABLE_GLOBAL_SKILLS`). */
|
|
11546
11664
|
const GITLABDUO_GLOBAL_SKILLS_DIR = (0, node_path.join)(GITLABDUO_DIR, "skills");
|
|
11547
11665
|
//#endregion
|
|
@@ -11703,6 +11821,239 @@ const CODEXCLI_OVERRIDE_KEYS = [
|
|
|
11703
11821
|
"approvals_reviewer",
|
|
11704
11822
|
"tui"
|
|
11705
11823
|
];
|
|
11824
|
+
const SINGLE_CHAR_ESCAPES = {
|
|
11825
|
+
"'": "'",
|
|
11826
|
+
"\"": "\"",
|
|
11827
|
+
"\\": "\\",
|
|
11828
|
+
b: "\b",
|
|
11829
|
+
f: "\f",
|
|
11830
|
+
n: "\n",
|
|
11831
|
+
r: "\r",
|
|
11832
|
+
t: " ",
|
|
11833
|
+
v: "\v"
|
|
11834
|
+
};
|
|
11835
|
+
const LINE_TERMINATORS = /* @__PURE__ */ new Set([
|
|
11836
|
+
"\n",
|
|
11837
|
+
"\r",
|
|
11838
|
+
"\u2028",
|
|
11839
|
+
"\u2029"
|
|
11840
|
+
]);
|
|
11841
|
+
const ID_START = /[\p{L}\p{Nl}$_]/u;
|
|
11842
|
+
const ID_CONTINUE = /[\p{L}\p{Nl}\p{Mn}\p{Mc}\p{Nd}\p{Pc}$_\u200C\u200D]/u;
|
|
11843
|
+
const WHITESPACE$1 = /[\t\n\v\f\r \u00A0\u2028\u2029\uFEFF\p{Zs}]/u;
|
|
11844
|
+
var Json5Reader = class {
|
|
11845
|
+
text;
|
|
11846
|
+
pos = 0;
|
|
11847
|
+
constructor(text) {
|
|
11848
|
+
this.text = text;
|
|
11849
|
+
}
|
|
11850
|
+
parse() {
|
|
11851
|
+
this.skipTrivia();
|
|
11852
|
+
const value = this.readValue(0);
|
|
11853
|
+
this.skipTrivia();
|
|
11854
|
+
if (this.pos < this.text.length) this.fail(`unexpected ${this.describe()} after the end of the document`);
|
|
11855
|
+
return value;
|
|
11856
|
+
}
|
|
11857
|
+
fail(message) {
|
|
11858
|
+
let line = 1;
|
|
11859
|
+
let column = 1;
|
|
11860
|
+
for (let i = 0; i < this.pos && i < this.text.length; i++) if (this.text[i] === "\n") {
|
|
11861
|
+
line++;
|
|
11862
|
+
column = 1;
|
|
11863
|
+
} else column++;
|
|
11864
|
+
throw new SyntaxError(`JSON5: ${message} at line ${line}, column ${column}`);
|
|
11865
|
+
}
|
|
11866
|
+
describe() {
|
|
11867
|
+
const char = this.text[this.pos];
|
|
11868
|
+
return char === void 0 ? "end of input" : `character ${JSON.stringify(char)}`;
|
|
11869
|
+
}
|
|
11870
|
+
peek(offset = 0) {
|
|
11871
|
+
return this.text[this.pos + offset];
|
|
11872
|
+
}
|
|
11873
|
+
skipTrivia() {
|
|
11874
|
+
while (this.pos < this.text.length) {
|
|
11875
|
+
const char = this.text[this.pos];
|
|
11876
|
+
if (WHITESPACE$1.test(char)) this.pos++;
|
|
11877
|
+
else if (char === "/" && this.peek(1) === "/") {
|
|
11878
|
+
this.pos += 2;
|
|
11879
|
+
while (this.pos < this.text.length && !LINE_TERMINATORS.has(this.text[this.pos])) this.pos++;
|
|
11880
|
+
} else if (char === "/" && this.peek(1) === "*") {
|
|
11881
|
+
const end = this.text.indexOf("*/", this.pos + 2);
|
|
11882
|
+
if (end === -1) this.fail("unterminated block comment");
|
|
11883
|
+
this.pos = end + 2;
|
|
11884
|
+
} else return;
|
|
11885
|
+
}
|
|
11886
|
+
}
|
|
11887
|
+
readValue(depth) {
|
|
11888
|
+
const char = this.peek();
|
|
11889
|
+
if (char === "{" || char === "[") {
|
|
11890
|
+
if (depth >= 256) this.fail(`document nests deeper than 256 levels`);
|
|
11891
|
+
return char === "{" ? this.readObject(depth + 1) : this.readArray(depth + 1);
|
|
11892
|
+
}
|
|
11893
|
+
if (char === "\"" || char === "'") return this.readString();
|
|
11894
|
+
if (char !== void 0 && /[0-9+\-.IN]/.test(char)) return this.readNumber();
|
|
11895
|
+
for (const [literal, value] of [
|
|
11896
|
+
["true", true],
|
|
11897
|
+
["false", false],
|
|
11898
|
+
["null", null]
|
|
11899
|
+
]) if (this.text.startsWith(literal, this.pos) && !this.isIdContinueAt(literal.length)) {
|
|
11900
|
+
this.pos += literal.length;
|
|
11901
|
+
return value;
|
|
11902
|
+
}
|
|
11903
|
+
return this.fail(`unexpected ${this.describe()}`);
|
|
11904
|
+
}
|
|
11905
|
+
isIdContinueAt(offset) {
|
|
11906
|
+
const char = this.peek(offset);
|
|
11907
|
+
return char !== void 0 && (ID_CONTINUE.test(char) || char === "\\");
|
|
11908
|
+
}
|
|
11909
|
+
readObject(depth) {
|
|
11910
|
+
const result = {};
|
|
11911
|
+
this.pos++;
|
|
11912
|
+
this.skipTrivia();
|
|
11913
|
+
while (this.peek() !== "}") {
|
|
11914
|
+
const key = this.readKey();
|
|
11915
|
+
this.skipTrivia();
|
|
11916
|
+
if (this.peek() !== ":") this.fail(`expected ':' after the key ${JSON.stringify(key)}`);
|
|
11917
|
+
this.pos++;
|
|
11918
|
+
this.skipTrivia();
|
|
11919
|
+
const value = this.readValue(depth);
|
|
11920
|
+
Object.defineProperty(result, key, {
|
|
11921
|
+
value,
|
|
11922
|
+
enumerable: true,
|
|
11923
|
+
writable: true,
|
|
11924
|
+
configurable: true
|
|
11925
|
+
});
|
|
11926
|
+
this.skipTrivia();
|
|
11927
|
+
if (this.peek() === ",") {
|
|
11928
|
+
this.pos++;
|
|
11929
|
+
this.skipTrivia();
|
|
11930
|
+
continue;
|
|
11931
|
+
}
|
|
11932
|
+
if (this.peek() !== "}") this.fail(`expected ',' or '}' but found ${this.describe()}`);
|
|
11933
|
+
}
|
|
11934
|
+
this.pos++;
|
|
11935
|
+
return result;
|
|
11936
|
+
}
|
|
11937
|
+
readArray(depth) {
|
|
11938
|
+
const result = [];
|
|
11939
|
+
this.pos++;
|
|
11940
|
+
this.skipTrivia();
|
|
11941
|
+
while (this.peek() !== "]") {
|
|
11942
|
+
result.push(this.readValue(depth));
|
|
11943
|
+
this.skipTrivia();
|
|
11944
|
+
if (this.peek() === ",") {
|
|
11945
|
+
this.pos++;
|
|
11946
|
+
this.skipTrivia();
|
|
11947
|
+
continue;
|
|
11948
|
+
}
|
|
11949
|
+
if (this.peek() !== "]") this.fail(`expected ',' or ']' but found ${this.describe()}`);
|
|
11950
|
+
}
|
|
11951
|
+
this.pos++;
|
|
11952
|
+
return result;
|
|
11953
|
+
}
|
|
11954
|
+
readKey() {
|
|
11955
|
+
const char = this.peek();
|
|
11956
|
+
if (char === "\"" || char === "'") return this.readString();
|
|
11957
|
+
let key = "";
|
|
11958
|
+
while (this.pos < this.text.length) {
|
|
11959
|
+
const start = this.pos;
|
|
11960
|
+
let next = this.peek();
|
|
11961
|
+
if (next === "\\") {
|
|
11962
|
+
if (this.peek(1) !== "u") this.fail("expected a \\u escape in an identifier key");
|
|
11963
|
+
this.pos += 2;
|
|
11964
|
+
next = this.readHex(4);
|
|
11965
|
+
} else this.pos++;
|
|
11966
|
+
if (!(key === "" ? ID_START.test(next) : ID_CONTINUE.test(next))) {
|
|
11967
|
+
this.pos = start;
|
|
11968
|
+
break;
|
|
11969
|
+
}
|
|
11970
|
+
key += next;
|
|
11971
|
+
}
|
|
11972
|
+
if (key === "") this.fail(`expected a key but found ${this.describe()}`);
|
|
11973
|
+
return key;
|
|
11974
|
+
}
|
|
11975
|
+
/** Reads `length` hex digits at the cursor and returns the character they encode. */
|
|
11976
|
+
readHex(length) {
|
|
11977
|
+
const digits = this.text.slice(this.pos, this.pos + length);
|
|
11978
|
+
if (digits.length !== length || !/^[0-9a-fA-F]+$/.test(digits)) this.fail(`expected ${length} hexadecimal digits`);
|
|
11979
|
+
this.pos += length;
|
|
11980
|
+
return String.fromCharCode(Number.parseInt(digits, 16));
|
|
11981
|
+
}
|
|
11982
|
+
readString() {
|
|
11983
|
+
const quote = this.peek();
|
|
11984
|
+
this.pos++;
|
|
11985
|
+
let result = "";
|
|
11986
|
+
while (true) {
|
|
11987
|
+
const char = this.peek();
|
|
11988
|
+
if (char === void 0) this.fail("unterminated string");
|
|
11989
|
+
if (char === quote) {
|
|
11990
|
+
this.pos++;
|
|
11991
|
+
return result;
|
|
11992
|
+
}
|
|
11993
|
+
if (char === "\n" || char === "\r") this.fail("unescaped line break in a string");
|
|
11994
|
+
if (char !== "\\") {
|
|
11995
|
+
result += char;
|
|
11996
|
+
this.pos++;
|
|
11997
|
+
continue;
|
|
11998
|
+
}
|
|
11999
|
+
this.pos++;
|
|
12000
|
+
const escape = this.peek();
|
|
12001
|
+
if (escape === void 0) this.fail("unterminated string");
|
|
12002
|
+
if (escape === "\r") this.pos += this.peek(1) === "\n" ? 2 : 1;
|
|
12003
|
+
else if (LINE_TERMINATORS.has(escape)) this.pos++;
|
|
12004
|
+
else if (escape === "x") {
|
|
12005
|
+
this.pos++;
|
|
12006
|
+
result += this.readHex(2);
|
|
12007
|
+
} else if (escape === "u") {
|
|
12008
|
+
this.pos++;
|
|
12009
|
+
result += this.readHex(4);
|
|
12010
|
+
} else if (escape === "0") {
|
|
12011
|
+
if (/[0-9]/.test(this.peek(1) ?? "")) this.fail("octal escapes are not allowed");
|
|
12012
|
+
result += "\0";
|
|
12013
|
+
this.pos++;
|
|
12014
|
+
} else if (/[1-9]/.test(escape)) this.fail("octal escapes are not allowed");
|
|
12015
|
+
else {
|
|
12016
|
+
result += SINGLE_CHAR_ESCAPES[escape] ?? escape;
|
|
12017
|
+
this.pos++;
|
|
12018
|
+
}
|
|
12019
|
+
}
|
|
12020
|
+
}
|
|
12021
|
+
readNumber() {
|
|
12022
|
+
const start = this.pos;
|
|
12023
|
+
let sign = 1;
|
|
12024
|
+
if (this.peek() === "+" || this.peek() === "-") {
|
|
12025
|
+
sign = this.peek() === "-" ? -1 : 1;
|
|
12026
|
+
this.pos++;
|
|
12027
|
+
}
|
|
12028
|
+
for (const [literal, value] of [["Infinity", Infinity], ["NaN", NaN]]) if (this.text.startsWith(literal, this.pos)) {
|
|
12029
|
+
if (this.isIdContinueAt(literal.length)) break;
|
|
12030
|
+
this.pos += literal.length;
|
|
12031
|
+
return sign * value;
|
|
12032
|
+
}
|
|
12033
|
+
if (this.peek() === "0" && (this.peek(1) === "x" || this.peek(1) === "X")) {
|
|
12034
|
+
this.pos += 2;
|
|
12035
|
+
const digits = /^[0-9a-fA-F]+/.exec(this.text.slice(this.pos))?.[0];
|
|
12036
|
+
if (digits === void 0) this.fail("expected hexadecimal digits");
|
|
12037
|
+
this.pos += digits.length;
|
|
12038
|
+
return sign * Number.parseInt(digits, 16);
|
|
12039
|
+
}
|
|
12040
|
+
const match = /^(?:(?:0|[1-9][0-9]*)(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?/.exec(this.text.slice(this.pos));
|
|
12041
|
+
if (match === null) {
|
|
12042
|
+
this.pos = start;
|
|
12043
|
+
this.fail(`invalid number`);
|
|
12044
|
+
}
|
|
12045
|
+
this.pos += match[0].length;
|
|
12046
|
+
if (this.isIdContinueAt(0)) this.fail(`invalid number`);
|
|
12047
|
+
return sign * Number(match[0]);
|
|
12048
|
+
}
|
|
12049
|
+
};
|
|
12050
|
+
/**
|
|
12051
|
+
* Parse a JSON5 document. Throws a `SyntaxError` naming the line and column of
|
|
12052
|
+
* the first problem.
|
|
12053
|
+
*/
|
|
12054
|
+
function parseJson5(text) {
|
|
12055
|
+
return new Json5Reader(text).parse();
|
|
12056
|
+
}
|
|
11706
12057
|
//#endregion
|
|
11707
12058
|
//#region src/utils/quote-value.ts
|
|
11708
12059
|
/**
|
|
@@ -11852,6 +12203,7 @@ function parseSharedConfig({ format, fileContent, filePath, invalidRootPolicy =
|
|
|
11852
12203
|
if (format === "yaml") parsed = loadYaml(fileContent);
|
|
11853
12204
|
else if (format === "toml") parsed = (0, smol_toml.parse)(fileContent);
|
|
11854
12205
|
else if (format === "json") parsed = JSON.parse(fileContent);
|
|
12206
|
+
else if (format === "json5") parsed = parseJson5(fileContent);
|
|
11855
12207
|
else if (jsoncParseErrors === "error") {
|
|
11856
12208
|
const errors = [];
|
|
11857
12209
|
parsed = (0, jsonc_parser.parse)(fileContent, errors, { allowTrailingComma: true });
|
|
@@ -12449,8 +12801,12 @@ function serializeSharedConfig({ format, document, existingContent, leadingKeys
|
|
|
12449
12801
|
format,
|
|
12450
12802
|
document
|
|
12451
12803
|
});
|
|
12452
|
-
if (format !== "jsonc" || existingContent.trim() === "") return whole;
|
|
12804
|
+
if (format !== "jsonc" && format !== "json5" || existingContent.trim() === "") return whole;
|
|
12453
12805
|
const wholeBecause = (reason) => {
|
|
12806
|
+
if (format === "json5") throw new Json5EditRefusedError({
|
|
12807
|
+
filePath,
|
|
12808
|
+
reason
|
|
12809
|
+
});
|
|
12454
12810
|
warnAboutWholeJsoncRewrite({
|
|
12455
12811
|
filePath,
|
|
12456
12812
|
reason,
|
|
@@ -12461,7 +12817,7 @@ function serializeSharedConfig({ format, document, existingContent, leadingKeys
|
|
|
12461
12817
|
try {
|
|
12462
12818
|
const errors = [];
|
|
12463
12819
|
const root = (0, jsonc_parser.parseTree)(existingContent, errors, { allowTrailingComma: true });
|
|
12464
|
-
if (root === void 0 || errors.length > 0) return wholeBecause("it could not be parsed");
|
|
12820
|
+
if (root === void 0 || errors.length > 0) return wholeBecause(format === "json5" ? "it uses JSON5-only syntax (such as unquoted keys, single quotes, hex numbers, Infinity or NaN)" : "it could not be parsed");
|
|
12465
12821
|
if (root.type !== "object") return wholeBecause("its root is not an object");
|
|
12466
12822
|
const uneditable = uneditableKeyOf(root);
|
|
12467
12823
|
if (uneditable !== void 0) {
|
|
@@ -12490,10 +12846,22 @@ function serializeSharedConfig({ format, document, existingContent, leadingKeys
|
|
|
12490
12846
|
leadingKeys
|
|
12491
12847
|
});
|
|
12492
12848
|
} catch (error) {
|
|
12849
|
+
if (error instanceof Json5EditRefusedError) throw error;
|
|
12493
12850
|
return wholeBecause(`the editor refused it (${stripControlCharacters(formatError(error))})`);
|
|
12494
12851
|
}
|
|
12495
12852
|
}
|
|
12496
12853
|
/**
|
|
12854
|
+
* Thrown instead of rewriting a `json5` file whole (see
|
|
12855
|
+
* {@link serializeSharedConfig}); nothing is written.
|
|
12856
|
+
*/
|
|
12857
|
+
var Json5EditRefusedError = class extends Error {
|
|
12858
|
+
constructor({ filePath, reason }) {
|
|
12859
|
+
const subject = filePath === void 0 ? "a shared JSON5 config file" : quoteForLog(filePath);
|
|
12860
|
+
super(`Refusing to update ${subject}: rulesync cannot edit it in place because ${reason}, and rewriting it whole would drop its comments and could change its values. Convert it to JSONC-compatible syntax (double-quoted keys and strings, decimal numbers; comments and trailing commas are fine), or add the entries by hand.`);
|
|
12861
|
+
this.name = "Json5EditRefusedError";
|
|
12862
|
+
}
|
|
12863
|
+
};
|
|
12864
|
+
/**
|
|
12497
12865
|
* Report, once per run and file, that a JSONC file is being written whole
|
|
12498
12866
|
* instead of edited in place. Named by `filePath` when the caller knows it;
|
|
12499
12867
|
* a caller serializing text it never read from disk has no name to give.
|
|
@@ -12556,6 +12924,7 @@ const KIMI_CODE_HOME_CONFIG_SHARED_FILE_KEY = "config.toml";
|
|
|
12556
12924
|
const REASONIX_PROJECT_CONFIG_SHARED_FILE_KEY = "reasonix.toml";
|
|
12557
12925
|
const REASONIX_GLOBAL_CONFIG_SHARED_FILE_KEY = ".reasonix/config.toml";
|
|
12558
12926
|
const ROVODEV_CONFIG_SHARED_FILE_KEY = ".rovodev/config.yml";
|
|
12927
|
+
const OPENCLAW_CONFIG_SHARED_FILE_KEY = ".openclaw/openclaw.json";
|
|
12559
12928
|
const POOL_PROJECT_SETTINGS_SHARED_FILE_KEY = ".poolside/settings.yaml";
|
|
12560
12929
|
const POOL_GLOBAL_SETTINGS_SHARED_FILE_KEY = ".config/poolside/settings.yaml";
|
|
12561
12930
|
/**
|
|
@@ -13011,7 +13380,12 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
13011
13380
|
format: "json",
|
|
13012
13381
|
features: { permissions: {
|
|
13013
13382
|
kind: "replace-owned-keys",
|
|
13014
|
-
ownedKeys: [
|
|
13383
|
+
ownedKeys: [
|
|
13384
|
+
"permissions",
|
|
13385
|
+
"requiredPlugins",
|
|
13386
|
+
"optionalPlugins",
|
|
13387
|
+
"forbiddenPlugins"
|
|
13388
|
+
]
|
|
13015
13389
|
} }
|
|
13016
13390
|
},
|
|
13017
13391
|
".config/devin/config.json": {
|
|
@@ -13159,10 +13533,27 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
13159
13533
|
},
|
|
13160
13534
|
permissions: {
|
|
13161
13535
|
kind: "replace-owned-keys",
|
|
13162
|
-
ownedKeys: [
|
|
13536
|
+
ownedKeys: [
|
|
13537
|
+
"permissions",
|
|
13538
|
+
"outputStyle",
|
|
13539
|
+
"general",
|
|
13540
|
+
"tools",
|
|
13541
|
+
"mcp",
|
|
13542
|
+
"agents",
|
|
13543
|
+
"autoMode",
|
|
13544
|
+
"agentsMdExcludes"
|
|
13545
|
+
]
|
|
13163
13546
|
}
|
|
13164
13547
|
}
|
|
13165
13548
|
},
|
|
13549
|
+
".trae/permission/global.json": {
|
|
13550
|
+
format: "json",
|
|
13551
|
+
invalidRootPolicy: "error",
|
|
13552
|
+
features: { permissions: {
|
|
13553
|
+
kind: "replace-owned-keys",
|
|
13554
|
+
ownedKeys: ["customProfiles", "resourceAuthorization"]
|
|
13555
|
+
} }
|
|
13556
|
+
},
|
|
13166
13557
|
".tabnine/agent/settings.json": {
|
|
13167
13558
|
format: "json",
|
|
13168
13559
|
invalidRootPolicy: "error",
|
|
@@ -13436,6 +13827,14 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
13436
13827
|
ownedKeys: ["toolPermissions"]
|
|
13437
13828
|
}
|
|
13438
13829
|
}
|
|
13830
|
+
},
|
|
13831
|
+
[OPENCLAW_CONFIG_SHARED_FILE_KEY]: {
|
|
13832
|
+
format: "json5",
|
|
13833
|
+
invalidRootPolicy: "error",
|
|
13834
|
+
features: { mcp: {
|
|
13835
|
+
kind: "replace-owned-keys",
|
|
13836
|
+
ownedKeys: ["mcp"]
|
|
13837
|
+
} }
|
|
13439
13838
|
}
|
|
13440
13839
|
};
|
|
13441
13840
|
/**
|
|
@@ -21978,6 +22377,190 @@ var KiroIdeCommand = class extends KiroCommand {
|
|
|
21978
22377
|
}
|
|
21979
22378
|
};
|
|
21980
22379
|
//#endregion
|
|
22380
|
+
//#region src/constants/lettacode-paths.ts
|
|
22381
|
+
const LETTACODE_DIR = ".letta";
|
|
22382
|
+
const LETTACODE_SETTINGS_FILE_NAME = "settings.json";
|
|
22383
|
+
const LETTACODE_SETTINGS_LOCAL_FILE_NAME = "settings.local.json";
|
|
22384
|
+
const LETTACODE_IGNORE_FILE_NAME = ".lettaignore";
|
|
22385
|
+
const LETTACODE_AGENTS_DIR_PATH = (0, node_path.join)(LETTACODE_DIR, "agents");
|
|
22386
|
+
const LETTACODE_PROJECT_SKILLS_DIR_PATH = (0, node_path.join)(".agents", "skills");
|
|
22387
|
+
const LETTACODE_GLOBAL_SKILLS_DIR_PATH = (0, node_path.join)(LETTACODE_DIR, "skills");
|
|
22388
|
+
const LETTACODE_PROJECT_COMMANDS_DIR_PATH = ".commands";
|
|
22389
|
+
const LETTACODE_GLOBAL_COMMANDS_DIR_PATH = (0, node_path.join)(LETTACODE_DIR, "commands");
|
|
22390
|
+
//#endregion
|
|
22391
|
+
//#region src/features/commands/lettacode-command.ts
|
|
22392
|
+
/**
|
|
22393
|
+
* The only frontmatter keys Letta Code reads from a custom command
|
|
22394
|
+
* (`parseCommandFile` in `src/cli/commands/custom.ts`). Anything else — e.g.
|
|
22395
|
+
* `allowed-tools`, `model` or `disable-model-invocation` copied over from a
|
|
22396
|
+
* Claude Code command — is parsed and ignored upstream.
|
|
22397
|
+
*/
|
|
22398
|
+
const LETTACODE_COMMAND_FRONTMATTER_KEYS = ["description", "argument-hint"];
|
|
22399
|
+
const LettacodeCommandFrontmatterSchema = zod_mini.z.looseObject({
|
|
22400
|
+
description: zod_mini.z.optional(zod_mini.z.string()),
|
|
22401
|
+
"argument-hint": zod_mini.z.optional(zod_mini.z.string())
|
|
22402
|
+
});
|
|
22403
|
+
function pickLettacodeFrontmatter({ section, relativeFilePath, logger }) {
|
|
22404
|
+
if (!isRecord$1(section)) return {};
|
|
22405
|
+
const picked = {};
|
|
22406
|
+
for (const [key, value] of Object.entries(section)) {
|
|
22407
|
+
if (LETTACODE_COMMAND_FRONTMATTER_KEYS.includes(key)) {
|
|
22408
|
+
picked[key] = value;
|
|
22409
|
+
continue;
|
|
22410
|
+
}
|
|
22411
|
+
warnWithFallback(logger, `Letta Code commands: dropping lettacode.${stripControlCharacters(key)} from ${quoteForLog(relativeFilePath)}: Letta Code only reads ${LETTACODE_COMMAND_FRONTMATTER_KEYS.join(" and ")}.`);
|
|
22412
|
+
}
|
|
22413
|
+
return picked;
|
|
22414
|
+
}
|
|
22415
|
+
/**
|
|
22416
|
+
* Letta Code custom slash commands: Markdown prompt files under `.commands/`
|
|
22417
|
+
* (project, at the working-directory root) and `~/.letta/commands/` (global),
|
|
22418
|
+
* discovered recursively. The file basename is the command id
|
|
22419
|
+
* (`git/commit.md` is invoked as `/commit`; `findCustomCommand` matches the
|
|
22420
|
+
* id only), so two files with the same basename collide and the first one
|
|
22421
|
+
* found wins; a project command wins over a global one with the same id. Frontmatter supports `description` (defaulting to the first body
|
|
22422
|
+
* line) and `argument-hint`; the body supports `$ARGUMENTS`, `$1`..`$9`,
|
|
22423
|
+
* `` !`command` `` and `@file`.
|
|
22424
|
+
*
|
|
22425
|
+
* `description` comes from the rulesync frontmatter; a `lettacode:` section
|
|
22426
|
+
* may set `argument-hint` (and override `description`). Other keys in that
|
|
22427
|
+
* section are dropped with a warning because Letta Code never reads them.
|
|
22428
|
+
*
|
|
22429
|
+
* @see https://docs.letta.com/platform/cli/slash-commands
|
|
22430
|
+
* @see https://github.com/letta-ai/letta-code/blob/main/src/cli/commands/custom.ts
|
|
22431
|
+
*/
|
|
22432
|
+
var LettacodeCommand = class LettacodeCommand extends ToolCommand {
|
|
22433
|
+
frontmatter;
|
|
22434
|
+
body;
|
|
22435
|
+
constructor({ frontmatter, body, ...rest }) {
|
|
22436
|
+
if (rest.validate) {
|
|
22437
|
+
const result = LettacodeCommandFrontmatterSchema.safeParse(frontmatter);
|
|
22438
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${(0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
|
|
22439
|
+
}
|
|
22440
|
+
super({
|
|
22441
|
+
...rest,
|
|
22442
|
+
fileContent: stringifyFrontmatter(body, frontmatter)
|
|
22443
|
+
});
|
|
22444
|
+
this.frontmatter = frontmatter;
|
|
22445
|
+
this.body = body;
|
|
22446
|
+
}
|
|
22447
|
+
static getSettablePaths({ global = false } = {}) {
|
|
22448
|
+
return { relativeDirPath: global ? LETTACODE_GLOBAL_COMMANDS_DIR_PATH : LETTACODE_PROJECT_COMMANDS_DIR_PATH };
|
|
22449
|
+
}
|
|
22450
|
+
getBody() {
|
|
22451
|
+
return this.body;
|
|
22452
|
+
}
|
|
22453
|
+
getFrontmatter() {
|
|
22454
|
+
return this.frontmatter;
|
|
22455
|
+
}
|
|
22456
|
+
toRulesyncCommand() {
|
|
22457
|
+
const { description, ...restFields } = this.frontmatter;
|
|
22458
|
+
const rulesyncFrontmatter = {
|
|
22459
|
+
targets: ["*"],
|
|
22460
|
+
description,
|
|
22461
|
+
...Object.keys(restFields).length > 0 && { lettacode: restFields }
|
|
22462
|
+
};
|
|
22463
|
+
return new RulesyncCommand({
|
|
22464
|
+
outputRoot: ".",
|
|
22465
|
+
frontmatter: rulesyncFrontmatter,
|
|
22466
|
+
body: this.body,
|
|
22467
|
+
relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath,
|
|
22468
|
+
relativeFilePath: this.relativeFilePath,
|
|
22469
|
+
fileContent: stringifyFrontmatter(this.body, rulesyncFrontmatter),
|
|
22470
|
+
validate: true
|
|
22471
|
+
});
|
|
22472
|
+
}
|
|
22473
|
+
static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false, logger }) {
|
|
22474
|
+
const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
|
|
22475
|
+
const lettacodeFields = pickLettacodeFrontmatter({
|
|
22476
|
+
section: rulesyncFrontmatter.lettacode,
|
|
22477
|
+
relativeFilePath: rulesyncCommand.getRelativeFilePath(),
|
|
22478
|
+
logger
|
|
22479
|
+
});
|
|
22480
|
+
const frontmatter = {
|
|
22481
|
+
...rulesyncFrontmatter.description !== void 0 && { description: rulesyncFrontmatter.description },
|
|
22482
|
+
...lettacodeFields
|
|
22483
|
+
};
|
|
22484
|
+
const paths = this.getSettablePaths({ global });
|
|
22485
|
+
return new LettacodeCommand({
|
|
22486
|
+
outputRoot,
|
|
22487
|
+
frontmatter,
|
|
22488
|
+
body: rulesyncCommand.getBody(),
|
|
22489
|
+
relativeDirPath: paths.relativeDirPath,
|
|
22490
|
+
relativeFilePath: rulesyncCommand.getRelativeFilePath(),
|
|
22491
|
+
validate,
|
|
22492
|
+
global
|
|
22493
|
+
});
|
|
22494
|
+
}
|
|
22495
|
+
validate() {
|
|
22496
|
+
if (!this.frontmatter) return {
|
|
22497
|
+
success: true,
|
|
22498
|
+
error: null
|
|
22499
|
+
};
|
|
22500
|
+
const result = LettacodeCommandFrontmatterSchema.safeParse(this.frontmatter);
|
|
22501
|
+
if (result.success) return {
|
|
22502
|
+
success: true,
|
|
22503
|
+
error: null
|
|
22504
|
+
};
|
|
22505
|
+
return {
|
|
22506
|
+
success: false,
|
|
22507
|
+
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
22508
|
+
};
|
|
22509
|
+
}
|
|
22510
|
+
/**
|
|
22511
|
+
* Warn when two targeted commands share a basename. Letta Code invokes a
|
|
22512
|
+
* command by its basename only, so `git/commit.md` and `release/commit.md`
|
|
22513
|
+
* are both `/commit` and only the first one Letta Code finds is reachable.
|
|
22514
|
+
* Both files are still written, so this warns instead of failing the run.
|
|
22515
|
+
*/
|
|
22516
|
+
static validateRulesyncCommands({ rulesyncCommands, logger }) {
|
|
22517
|
+
const pathsById = /* @__PURE__ */ new Map();
|
|
22518
|
+
for (const command of rulesyncCommands) {
|
|
22519
|
+
if (!this.isTargetedByRulesyncCommand(command)) continue;
|
|
22520
|
+
const relativeFilePath = command.getRelativeFilePath();
|
|
22521
|
+
const id = (0, node_path.basename)(relativeFilePath, ".md");
|
|
22522
|
+
pathsById.set(id, [...pathsById.get(id) ?? [], relativeFilePath]);
|
|
22523
|
+
}
|
|
22524
|
+
for (const [id, paths] of pathsById) {
|
|
22525
|
+
if (paths.length < 2) continue;
|
|
22526
|
+
logger.warn(`Letta Code invokes commands by file basename, so ${paths.map((path) => quoteForLog(path)).join(", ")} are all /${stripControlCharacters(id)} and only one of them is reachable. Rename them to make each invocable.`);
|
|
22527
|
+
}
|
|
22528
|
+
}
|
|
22529
|
+
static isTargetedByRulesyncCommand(rulesyncCommand) {
|
|
22530
|
+
return this.isTargetedByRulesyncCommandDefault({
|
|
22531
|
+
rulesyncCommand,
|
|
22532
|
+
toolTarget: "lettacode"
|
|
22533
|
+
});
|
|
22534
|
+
}
|
|
22535
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
|
|
22536
|
+
const paths = this.getSettablePaths({ global });
|
|
22537
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
22538
|
+
const { frontmatter, body } = parseFrontmatter(await readFileContent(filePath), filePath);
|
|
22539
|
+
const result = LettacodeCommandFrontmatterSchema.safeParse(frontmatter);
|
|
22540
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
|
|
22541
|
+
return new LettacodeCommand({
|
|
22542
|
+
outputRoot,
|
|
22543
|
+
relativeDirPath: paths.relativeDirPath,
|
|
22544
|
+
relativeFilePath,
|
|
22545
|
+
frontmatter: result.data,
|
|
22546
|
+
body: body.trim(),
|
|
22547
|
+
validate,
|
|
22548
|
+
global
|
|
22549
|
+
});
|
|
22550
|
+
}
|
|
22551
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
22552
|
+
return new LettacodeCommand({
|
|
22553
|
+
outputRoot,
|
|
22554
|
+
relativeDirPath,
|
|
22555
|
+
relativeFilePath,
|
|
22556
|
+
frontmatter: {},
|
|
22557
|
+
body: "",
|
|
22558
|
+
validate: false,
|
|
22559
|
+
global
|
|
22560
|
+
});
|
|
22561
|
+
}
|
|
22562
|
+
};
|
|
22563
|
+
//#endregion
|
|
21981
22564
|
//#region src/constants/mimocode-paths.ts
|
|
21982
22565
|
/**
|
|
21983
22566
|
* MiMo Code (Xiaomi's OpenCode fork) reads `.mimocode/` in the project and
|
|
@@ -21995,7 +22578,8 @@ const MIMOCODE_LAYOUT = {
|
|
|
21995
22578
|
configDir: MIMOCODE_DIR,
|
|
21996
22579
|
jsonFileName: "mimocode.json",
|
|
21997
22580
|
jsoncFileName: "mimocode.jsonc",
|
|
21998
|
-
readsV2ConfigSpellings: false
|
|
22581
|
+
readsV2ConfigSpellings: false,
|
|
22582
|
+
readsDotDirConfig: false
|
|
21999
22583
|
};
|
|
22000
22584
|
//#endregion
|
|
22001
22585
|
//#region src/constants/opencode-paths.ts
|
|
@@ -22022,7 +22606,8 @@ const OPENCODE_LAYOUT = {
|
|
|
22022
22606
|
configDir: ".",
|
|
22023
22607
|
jsonFileName: OPENCODE_JSON_FILE_NAME,
|
|
22024
22608
|
jsoncFileName: OPENCODE_JSONC_FILE_NAME,
|
|
22025
|
-
readsV2ConfigSpellings: true
|
|
22609
|
+
readsV2ConfigSpellings: true,
|
|
22610
|
+
readsDotDirConfig: true
|
|
22026
22611
|
};
|
|
22027
22612
|
//#endregion
|
|
22028
22613
|
//#region src/features/opencode-config.ts
|
|
@@ -22056,6 +22641,107 @@ async function readOpencodeConfig({ outputRoot, global = false, layout = OPENCOD
|
|
|
22056
22641
|
});
|
|
22057
22642
|
}
|
|
22058
22643
|
/**
|
|
22644
|
+
* Reads the project-scope config files OpenCode merges over the root
|
|
22645
|
+
* `opencode.json(c)`: `.opencode/opencode.json`, then `.opencode/opencode.jsonc`,
|
|
22646
|
+
* in the order OpenCode loads them (a later file wins). Returns an empty list
|
|
22647
|
+
* in global mode and for layouts without this location (MiMo Code).
|
|
22648
|
+
*
|
|
22649
|
+
* @see https://opencode.ai/docs/config/#precedence-order
|
|
22650
|
+
* @see https://github.com/anomalyco/opencode/blob/v1.19.3/packages/opencode/src/config/config.ts
|
|
22651
|
+
*/
|
|
22652
|
+
async function readOpencodeDotDirConfigLayers({ outputRoot, global = false, layout = OPENCODE_LAYOUT }) {
|
|
22653
|
+
if (global || !layout.readsDotDirConfig) return [];
|
|
22654
|
+
const configDir = (0, node_path.join)(outputRoot, layout.dir);
|
|
22655
|
+
const layers = [];
|
|
22656
|
+
for (const fileName of [layout.jsonFileName, layout.jsoncFileName]) {
|
|
22657
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(configDir, fileName));
|
|
22658
|
+
if (fileContent === null) continue;
|
|
22659
|
+
layers.push({
|
|
22660
|
+
relativeFilePath: (0, node_path.join)(layout.dir, fileName),
|
|
22661
|
+
configDir,
|
|
22662
|
+
config: parseSharedConfig({
|
|
22663
|
+
format: "jsonc",
|
|
22664
|
+
fileContent
|
|
22665
|
+
})
|
|
22666
|
+
});
|
|
22667
|
+
}
|
|
22668
|
+
return layers;
|
|
22669
|
+
}
|
|
22670
|
+
/**
|
|
22671
|
+
* Reads every OpenCode config file that applies at this scope, lowest
|
|
22672
|
+
* precedence first: the root config (see {@link readOpencodeConfig}), then the
|
|
22673
|
+
* `.opencode/` files (see {@link readOpencodeDotDirConfigLayers}).
|
|
22674
|
+
*/
|
|
22675
|
+
async function readOpencodeConfigLayers({ outputRoot, global = false, layout = OPENCODE_LAYOUT }) {
|
|
22676
|
+
return [{
|
|
22677
|
+
configDir: getOpencodeConfigDir({
|
|
22678
|
+
outputRoot,
|
|
22679
|
+
global,
|
|
22680
|
+
layout
|
|
22681
|
+
}),
|
|
22682
|
+
config: await readOpencodeConfig({
|
|
22683
|
+
outputRoot,
|
|
22684
|
+
global,
|
|
22685
|
+
layout
|
|
22686
|
+
})
|
|
22687
|
+
}, ...await readOpencodeDotDirConfigLayers({
|
|
22688
|
+
outputRoot,
|
|
22689
|
+
global,
|
|
22690
|
+
layout
|
|
22691
|
+
})];
|
|
22692
|
+
}
|
|
22693
|
+
/**
|
|
22694
|
+
* Deep-merges `source` over `target` the way OpenCode merges its config files
|
|
22695
|
+
* (remeda's `mergeDeep`): plain objects merge key by key, and any other value —
|
|
22696
|
+
* arrays included — replaces the target's.
|
|
22697
|
+
*/
|
|
22698
|
+
function mergeOpencodeConfigValues(target, source) {
|
|
22699
|
+
if (!isRecord$1(target) || !isRecord$1(source)) return source;
|
|
22700
|
+
const merged = {};
|
|
22701
|
+
for (const [key, value] of Object.entries(target)) setOwnEntry(merged, key, value);
|
|
22702
|
+
for (const [key, value] of Object.entries(source)) setOwnEntry(merged, key, Object.hasOwn(target, key) ? mergeOpencodeConfigValues(target[key], value) : value);
|
|
22703
|
+
return merged;
|
|
22704
|
+
}
|
|
22705
|
+
/**
|
|
22706
|
+
* Merges per-file entry maps (the `command` or `agent` entries of each config
|
|
22707
|
+
* layer, lowest precedence first) into one, as OpenCode does. Returns `null`
|
|
22708
|
+
* when no layer has entries.
|
|
22709
|
+
*/
|
|
22710
|
+
function mergeOpencodeEntryLayers(layers) {
|
|
22711
|
+
let merged = null;
|
|
22712
|
+
for (const entries of layers) {
|
|
22713
|
+
if (entries === null) continue;
|
|
22714
|
+
const next = mergeOpencodeConfigValues(merged ?? {}, entries);
|
|
22715
|
+
merged = isRecord$1(next) ? next : merged;
|
|
22716
|
+
}
|
|
22717
|
+
return merged;
|
|
22718
|
+
}
|
|
22719
|
+
/**
|
|
22720
|
+
* Warns when a `.opencode/opencode.json(c)` file sets entries that the
|
|
22721
|
+
* generated root config also sets. OpenCode merges those files over the root
|
|
22722
|
+
* config, so their entries win and the generated ones never take effect.
|
|
22723
|
+
*
|
|
22724
|
+
* `generated` maps each top-level key rulesync writes (`mcp`, `permission`) to
|
|
22725
|
+
* its generated value. `overlays` are the `.opencode/` layers, already lowered
|
|
22726
|
+
* by the caller where needed.
|
|
22727
|
+
*/
|
|
22728
|
+
function warnOpencodeDotDirOverrides({ generated, overlays, rootFileName, logger }) {
|
|
22729
|
+
if (!logger) return;
|
|
22730
|
+
for (const overlay of overlays) {
|
|
22731
|
+
const overridden = [];
|
|
22732
|
+
for (const [key, value] of Object.entries(generated)) {
|
|
22733
|
+
if (value === void 0 || !Object.hasOwn(overlay.config, key)) continue;
|
|
22734
|
+
const overlayValue = overlay.config[key];
|
|
22735
|
+
if (!isRecord$1(value) || !isRecord$1(overlayValue)) {
|
|
22736
|
+
overridden.push(`"${key}"`);
|
|
22737
|
+
continue;
|
|
22738
|
+
}
|
|
22739
|
+
for (const name of Object.keys(value)) if (Object.hasOwn(overlayValue, name)) overridden.push(`"${key}" > ${quoteValueForWarning(name)}`);
|
|
22740
|
+
}
|
|
22741
|
+
if (overridden.length > 0) logger.warn(`OpenCode merges "${toPosixPath(overlay.relativeFilePath)}" over the generated "${rootFileName}", so its values for these generated entries take precedence: ${overridden.join(", ")}.`);
|
|
22742
|
+
}
|
|
22743
|
+
}
|
|
22744
|
+
/**
|
|
22059
22745
|
* Narrows an unknown value to a plain record of entries keyed by name, as used
|
|
22060
22746
|
* for OpenCode's `command` / `agent` config sections. Returns `null` when the
|
|
22061
22747
|
* value is not a usable object.
|
|
@@ -22498,8 +23184,9 @@ var OpenCodeCommand = class extends ToolCommand {
|
|
|
22498
23184
|
});
|
|
22499
23185
|
}
|
|
22500
23186
|
/**
|
|
22501
|
-
* Imports commands defined inline in `opencode.json` / `opencode.jsonc`
|
|
22502
|
-
*
|
|
23187
|
+
* Imports commands defined inline in `opencode.json` / `opencode.jsonc` (and,
|
|
23188
|
+
* at project scope, `.opencode/opencode.json(c)`, which OpenCode merges over
|
|
23189
|
+
* them) under the top-level `command` key — or its V2 spelling `commands`, see
|
|
22503
23190
|
* `getOpencodeCommandEntries` — (in addition to the Markdown files under
|
|
22504
23191
|
* `.opencode/commands/`). Each entry's `template` becomes the command body,
|
|
22505
23192
|
* while `description` / `agent` / `model` / `variant` / `subtask` map to the
|
|
@@ -22512,14 +23199,14 @@ var OpenCodeCommand = class extends ToolCommand {
|
|
|
22512
23199
|
* @see https://opencode.ai/docs/commands/#json
|
|
22513
23200
|
*/
|
|
22514
23201
|
static async loadAdditionalImportFiles({ outputRoot = process.cwd(), global = false } = {}) {
|
|
22515
|
-
const commandEntries =
|
|
22516
|
-
|
|
22517
|
-
|
|
22518
|
-
global,
|
|
22519
|
-
layout: this.layout
|
|
22520
|
-
}),
|
|
23202
|
+
const commandEntries = mergeOpencodeEntryLayers((await readOpencodeConfigLayers({
|
|
23203
|
+
outputRoot,
|
|
23204
|
+
global,
|
|
22521
23205
|
layout: this.layout
|
|
22522
|
-
})
|
|
23206
|
+
})).map(({ config }) => getOpencodeCommandEntries({
|
|
23207
|
+
config,
|
|
23208
|
+
layout: this.layout
|
|
23209
|
+
})));
|
|
22523
23210
|
if (!commandEntries) return [];
|
|
22524
23211
|
const paths = this.getSettablePaths({ global });
|
|
22525
23212
|
const commands = [];
|
|
@@ -23905,6 +24592,7 @@ var TaktCommand = class TaktCommand extends ToolCommand {
|
|
|
23905
24592
|
* @see https://docs.trae.ai/ide/add-mcp-servers?_lang=en
|
|
23906
24593
|
* @see https://docs.trae.ai/ide/slash-commands?_lang=en
|
|
23907
24594
|
* @see https://docs.trae.ai/ide/ignore-files?_lang=en
|
|
24595
|
+
* @see https://docs.trae.ai/ide/permission-and-approval?_lang=en
|
|
23908
24596
|
*/
|
|
23909
24597
|
const TRAE_DIR = ".trae";
|
|
23910
24598
|
/** Project MCP servers (`mcpServers` object), project scope only. */
|
|
@@ -23918,6 +24606,13 @@ const TRAE_SKILLS_DIR_PATH = (0, node_path.join)(TRAE_DIR, "skills");
|
|
|
23918
24606
|
const TRAE_COMMANDS_DIR_PATH = (0, node_path.join)(TRAE_DIR, "commands");
|
|
23919
24607
|
/** Indexing ignore list (gitignore syntax), `.trae/.ignore`, project scope only. */
|
|
23920
24608
|
const TRAE_IGNORE_FILE_NAME = ".ignore";
|
|
24609
|
+
/**
|
|
24610
|
+
* Global permission policy: `~/.trae/permission/global.json`, user scope only. It
|
|
24611
|
+
* also holds the custom permission mode (`customProfiles`), so it is edited in
|
|
24612
|
+
* place rather than owned outright.
|
|
24613
|
+
*/
|
|
24614
|
+
const TRAE_PERMISSION_DIR_PATH = (0, node_path.join)(TRAE_DIR, "permission");
|
|
24615
|
+
const TRAE_PERMISSION_FILE_NAME = "global.json";
|
|
23921
24616
|
//#endregion
|
|
23922
24617
|
//#region src/features/commands/trae-command.ts
|
|
23923
24618
|
const TraeCommandFrontmatterSchema = zod_mini.z.looseObject({
|
|
@@ -24683,6 +25378,17 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
|
|
|
24683
25378
|
supportsSubdirectory: false
|
|
24684
25379
|
}
|
|
24685
25380
|
}],
|
|
25381
|
+
["lettacode", {
|
|
25382
|
+
class: LettacodeCommand,
|
|
25383
|
+
meta: {
|
|
25384
|
+
extension: "md",
|
|
25385
|
+
supportsProject: true,
|
|
25386
|
+
supportsGlobal: true,
|
|
25387
|
+
isSimulated: false,
|
|
25388
|
+
supportsSubdirectory: true,
|
|
25389
|
+
committedOutput: true
|
|
25390
|
+
}
|
|
25391
|
+
}],
|
|
24686
25392
|
["mimocode", {
|
|
24687
25393
|
class: MimocodeCommand,
|
|
24688
25394
|
meta: {
|
|
@@ -24992,12 +25698,25 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
24992
25698
|
return rulesyncCommands;
|
|
24993
25699
|
}
|
|
24994
25700
|
/**
|
|
25701
|
+
* Whether {@link loadToolFiles} must not scan the tool's output tree at all.
|
|
25702
|
+
* `skipToolFileScan` targets never scan it; a `committedOutput` target is not
|
|
25703
|
+
* swept at project scope, because nothing on disk tells a hand-written
|
|
25704
|
+
* command in such a committed root apart from one rulesync generated.
|
|
25705
|
+
*/
|
|
25706
|
+
skipsToolFileScan({ factory, forDeletion }) {
|
|
25707
|
+
if (factory.meta.skipToolFileScan) return true;
|
|
25708
|
+
return forDeletion && factory.meta.committedOutput === true && !this.global;
|
|
25709
|
+
}
|
|
25710
|
+
/**
|
|
24995
25711
|
* Implementation of abstract method from FeatureProcessor
|
|
24996
25712
|
* Load tool-specific command configurations and parse them into ToolCommand instances
|
|
24997
25713
|
*/
|
|
24998
25714
|
async loadToolFiles({ forDeletion = false } = {}) {
|
|
24999
25715
|
const factory = this.getFactory(this.toolTarget);
|
|
25000
|
-
if (
|
|
25716
|
+
if (this.skipsToolFileScan({
|
|
25717
|
+
factory,
|
|
25718
|
+
forDeletion
|
|
25719
|
+
})) return [];
|
|
25001
25720
|
const paths = factory.class.getSettablePaths({ global: this.global });
|
|
25002
25721
|
const outputRootFull = (0, node_path.join)(this.outputRoot, paths.relativeDirPath);
|
|
25003
25722
|
const commandFilePaths = await findFilesByGlobs(factory.meta.supportsSubdirectory ? `**/*.${factory.meta.extension}` : `*.${factory.meta.extension}`, {
|
|
@@ -26714,13 +27433,48 @@ function emitAllPassthroughFields({ def, hookType, eventName, converterConfig, w
|
|
|
26714
27433
|
})
|
|
26715
27434
|
};
|
|
26716
27435
|
}
|
|
27436
|
+
/**
|
|
27437
|
+
* Why a definition must not be emitted for this tool, or `undefined` when it
|
|
27438
|
+
* can be: it carries a restricting field the tool refuses to honour (see
|
|
27439
|
+
* `skippedWhenFields`).
|
|
27440
|
+
*/
|
|
27441
|
+
function describeExportSkipReason({ def, hookType, eventName, converterConfig }) {
|
|
27442
|
+
const field = converterConfig.skippedWhenFields?.find((name) => isNonEmptyString(def[name]));
|
|
27443
|
+
if (field === void 0) return;
|
|
27444
|
+
return `Skipping a "${eventName}" ${hookType} hook (${quoteValueForWarning(def.command ?? def.url)}): this tool does not support its "${field}" condition (${quoteValueForWarning(def[field])}), and without it the hook would run on every call its matcher selects.`;
|
|
27445
|
+
}
|
|
27446
|
+
/**
|
|
27447
|
+
* Whether a definition can be emitted for this tool: its hook type is one the
|
|
27448
|
+
* tool supports, and it carries no restricting field the tool refuses to
|
|
27449
|
+
* honour (warned about here).
|
|
27450
|
+
*/
|
|
27451
|
+
function isEmittableDefinition({ def, hookType, eventName, converterConfig, warn }) {
|
|
27452
|
+
if (!isSupportedHookType({
|
|
27453
|
+
type: hookType,
|
|
27454
|
+
converterConfig
|
|
27455
|
+
})) return false;
|
|
27456
|
+
const skipReason = describeExportSkipReason({
|
|
27457
|
+
def,
|
|
27458
|
+
hookType,
|
|
27459
|
+
eventName,
|
|
27460
|
+
converterConfig
|
|
27461
|
+
});
|
|
27462
|
+
if (skipReason !== void 0) {
|
|
27463
|
+
warn?.(skipReason);
|
|
27464
|
+
return false;
|
|
27465
|
+
}
|
|
27466
|
+
return true;
|
|
27467
|
+
}
|
|
26717
27468
|
function buildToolHooks({ defs, eventName, converterConfig, warn }) {
|
|
26718
27469
|
const hooks = [];
|
|
26719
27470
|
for (const def of defs) {
|
|
26720
27471
|
const hookType = def.type ?? "command";
|
|
26721
|
-
if (!
|
|
26722
|
-
|
|
26723
|
-
|
|
27472
|
+
if (!isEmittableDefinition({
|
|
27473
|
+
def,
|
|
27474
|
+
hookType,
|
|
27475
|
+
eventName,
|
|
27476
|
+
converterConfig,
|
|
27477
|
+
warn
|
|
26724
27478
|
})) continue;
|
|
26725
27479
|
const command = applyCommandPrefix({
|
|
26726
27480
|
def,
|
|
@@ -27292,7 +28046,10 @@ function toolHooksToCanonical({ hooks, converterConfig, logger }) {
|
|
|
27292
28046
|
warn
|
|
27293
28047
|
}));
|
|
27294
28048
|
}
|
|
27295
|
-
if (defs.length > 0)
|
|
28049
|
+
if (defs.length > 0) {
|
|
28050
|
+
const existing = Object.hasOwn(canonical, eventName) ? canonical[eventName] : void 0;
|
|
28051
|
+
canonical[eventName] = existing ? [...existing, ...defs] : defs;
|
|
28052
|
+
}
|
|
27296
28053
|
}
|
|
27297
28054
|
return canonical;
|
|
27298
28055
|
}
|
|
@@ -32002,6 +32759,16 @@ const GROKCLI_CONVERTER_CONFIG = {
|
|
|
32002
32759
|
tool: "env",
|
|
32003
32760
|
commandOnly: true
|
|
32004
32761
|
}],
|
|
32762
|
+
arrayPassthroughFields: [{
|
|
32763
|
+
canonical: "args",
|
|
32764
|
+
tool: "args",
|
|
32765
|
+
commandOnly: true
|
|
32766
|
+
}],
|
|
32767
|
+
skippedWhenFields: ["if"],
|
|
32768
|
+
stringPassthroughFields: [{
|
|
32769
|
+
canonical: "if",
|
|
32770
|
+
tool: "if"
|
|
32771
|
+
}],
|
|
32005
32772
|
noMatcherEvents: /* @__PURE__ */ new Set(["stop", "beforeSubmitPrompt"])
|
|
32006
32773
|
};
|
|
32007
32774
|
/**
|
|
@@ -33498,15 +34265,6 @@ var KiroHooks = class KiroHooks extends ToolHooks {
|
|
|
33498
34265
|
}
|
|
33499
34266
|
};
|
|
33500
34267
|
//#endregion
|
|
33501
|
-
//#region src/constants/lettacode-paths.ts
|
|
33502
|
-
const LETTACODE_DIR = ".letta";
|
|
33503
|
-
const LETTACODE_SETTINGS_FILE_NAME = "settings.json";
|
|
33504
|
-
const LETTACODE_SETTINGS_LOCAL_FILE_NAME = "settings.local.json";
|
|
33505
|
-
const LETTACODE_IGNORE_FILE_NAME = ".lettaignore";
|
|
33506
|
-
const LETTACODE_AGENTS_DIR_PATH = (0, node_path.join)(LETTACODE_DIR, "agents");
|
|
33507
|
-
const LETTACODE_PROJECT_SKILLS_DIR_PATH = (0, node_path.join)(".agents", "skills");
|
|
33508
|
-
const LETTACODE_GLOBAL_SKILLS_DIR_PATH = (0, node_path.join)(LETTACODE_DIR, "skills");
|
|
33509
|
-
//#endregion
|
|
33510
34268
|
//#region src/features/hooks/lettacode-hooks.ts
|
|
33511
34269
|
const LETTACODE_CONVERTER_CONFIG = {
|
|
33512
34270
|
supportedEvents: LETTACODE_HOOK_EVENTS,
|
|
@@ -43195,8 +43953,10 @@ var CortexcodeMcp = class CortexcodeMcp extends ToolMcp {
|
|
|
43195
43953
|
/**
|
|
43196
43954
|
* Crush-only MCP fields with no canonical counterpart, authored under a
|
|
43197
43955
|
* `crush`-prefixed key in `.rulesync/mcp.jsonc` and written under Crush's own
|
|
43198
|
-
* name: `oauth*` drive Crush's OAuth 2.1 flow for HTTP servers
|
|
43199
|
-
* `sessionless` marks a server that issues no `Mcp-Session-Id
|
|
43956
|
+
* name: `oauth*` drive Crush's OAuth 2.1 flow for HTTP servers,
|
|
43957
|
+
* `sessionless` marks a server that issues no `Mcp-Session-Id`; and
|
|
43958
|
+
* `channel_enabled` / `channel_reply` (v0.97.0) turn a server that implements
|
|
43959
|
+
* Claude Channels into a channel and route replies back through its tools.
|
|
43200
43960
|
* `RulesyncMcp.getMcpServers()` strips both spellings so the client secret
|
|
43201
43961
|
* never reaches another tool's config; `fromRulesyncMcp` re-merges them from
|
|
43202
43962
|
* the raw source JSON, honouring the raw Crush spelling as a fallback for an
|
|
@@ -43235,6 +43995,18 @@ const CRUSH_ONLY_KEYS = [
|
|
|
43235
43995
|
crush: "sessionless",
|
|
43236
43996
|
accepts: (v) => typeof v === "boolean",
|
|
43237
43997
|
rawFallback: true
|
|
43998
|
+
},
|
|
43999
|
+
{
|
|
44000
|
+
canonical: "crushChannelEnabled",
|
|
44001
|
+
crush: "channel_enabled",
|
|
44002
|
+
accepts: (v) => typeof v === "boolean",
|
|
44003
|
+
rawFallback: true
|
|
44004
|
+
},
|
|
44005
|
+
{
|
|
44006
|
+
canonical: "crushChannelReply",
|
|
44007
|
+
crush: "channel_reply",
|
|
44008
|
+
accepts: (v) => CrushChannelReplySchema.safeParse(v).success,
|
|
44009
|
+
rawFallback: true
|
|
43238
44010
|
}
|
|
43239
44011
|
];
|
|
43240
44012
|
/**
|
|
@@ -43242,12 +44014,16 @@ const CRUSH_ONLY_KEYS = [
|
|
|
43242
44014
|
* canonical name, so `convertToCrushFormat` sees them after the shared map
|
|
43243
44015
|
* stripped them.
|
|
43244
44016
|
*/
|
|
43245
|
-
function readCrushOnlyKeys(rawServer) {
|
|
44017
|
+
function readCrushOnlyKeys({ serverName, rawServer, logger }) {
|
|
43246
44018
|
const result = {};
|
|
43247
44019
|
if (!isRecord$1(rawServer)) return result;
|
|
43248
44020
|
for (const { canonical, crush, accepts, rawFallback } of CRUSH_ONLY_KEYS) {
|
|
43249
44021
|
const value = accepts(rawServer[canonical]) ? rawServer[canonical] : rawFallback && accepts(rawServer[crush]) ? rawServer[crush] : void 0;
|
|
43250
44022
|
if (value !== void 0) result[canonical] = value;
|
|
44023
|
+
else if (canonical === "crushChannelReply") {
|
|
44024
|
+
const rejected = [canonical, crush].find((key) => rawServer[key] !== void 0);
|
|
44025
|
+
if (rejected !== void 0) logger?.warn(`Crush MCP server "${serverName}": ignoring "${rejected}" because it does not match Crush's channel_reply shape ({ user?, group?: { tool, target_param?, target_meta? }, message_param?, suppress_tools? }).`);
|
|
44026
|
+
}
|
|
43251
44027
|
}
|
|
43252
44028
|
return result;
|
|
43253
44029
|
}
|
|
@@ -43372,8 +44148,8 @@ function convertToCrushFormat(mcpServers, logger) {
|
|
|
43372
44148
|
* servers. `stdio` is Crush's default `type`, so it is dropped (a canonical
|
|
43373
44149
|
* server with a `command` is stdio already); `http` and `sse` are kept as the
|
|
43374
44150
|
* canonical transport names; `disabled_tools`/`enabled_tools` become the
|
|
43375
|
-
* canonical `disabledTools`/`enabledTools`; the Crush-only OAuth
|
|
43376
|
-
* `sessionless` fields are lifted into their `crush*` authoring keys (a value
|
|
44151
|
+
* canonical `disabledTools`/`enabledTools`; the Crush-only OAuth,
|
|
44152
|
+
* `sessionless` and channel fields are lifted into their `crush*` authoring keys (a value
|
|
43377
44153
|
* of the wrong type is dropped, as Crush itself would refuse it), and any
|
|
43378
44154
|
* other key is kept verbatim so an import preserves what a hand-authored
|
|
43379
44155
|
* entry declared.
|
|
@@ -43464,7 +44240,11 @@ var CrushMcp = class CrushMcp extends ToolMcp {
|
|
|
43464
44240
|
});
|
|
43465
44241
|
const converted = convertToCrushFormat(Object.fromEntries(Object.entries(rulesyncMcp.getMcpServers()).map(([serverName, serverConfig]) => [serverName, {
|
|
43466
44242
|
...serverConfig,
|
|
43467
|
-
...readCrushOnlyKeys(
|
|
44243
|
+
...readCrushOnlyKeys({
|
|
44244
|
+
serverName,
|
|
44245
|
+
rawServer: rulesyncMcp.getRawMcpServer(serverName),
|
|
44246
|
+
logger
|
|
44247
|
+
})
|
|
43468
44248
|
}])), logger);
|
|
43469
44249
|
return new CrushMcp({
|
|
43470
44250
|
outputRoot,
|
|
@@ -44057,12 +44837,19 @@ var DevinPluginMcp = class DevinPluginMcp extends DevinMcp {
|
|
|
44057
44837
|
* only the default location. Project-scoped assets live under a `.dsh/`
|
|
44058
44838
|
* directory at the project root.
|
|
44059
44839
|
*
|
|
44060
|
-
* Rules: `dsh-agent-instructions` loads the user-global
|
|
44061
|
-
*
|
|
44062
|
-
*
|
|
44063
|
-
*
|
|
44064
|
-
*
|
|
44065
|
-
*
|
|
44840
|
+
* Rules: `dsh-agent-instructions` loads the user-global chain —
|
|
44841
|
+
* `$DSH_HOME/AGENTS.md`, then `<agentsHome>/AGENTS.md` (`$DSH_AGENTS_HOME`,
|
|
44842
|
+
* default `~/.agents`, since dsh-v0.2.1-alpha.2) — then the project chain of
|
|
44843
|
+
* `AGENTS.md` / `CLAUDE.md` files plus their `AGENTS.local.md` /
|
|
44844
|
+
* `CLAUDE.local.md` overlays from the project root (the nearest ancestor
|
|
44845
|
+
* holding `.git`) down to the session cwd, broad to specific. Neither
|
|
44846
|
+
* user-global root has a `.local.md` overlay. The two user-global roots share
|
|
44847
|
+
* one dedupe group, so the shared-root file collapses behind the harness-home
|
|
44848
|
+
* file only when both match after trimming; otherwise both load. Rulesync
|
|
44849
|
+
* writes only `$DSH_HOME/AGENTS.md` globally (never the shared
|
|
44850
|
+
* `~/.agents/AGENTS.md`), and `AGENTS.md` / `AGENTS.local.md` in the project;
|
|
44851
|
+
* `CLAUDE.md` is the `claudecode` target's file, and a `CLAUDE.md` duplicating
|
|
44852
|
+
* its sibling `AGENTS.md` renders once.
|
|
44066
44853
|
*
|
|
44067
44854
|
* Skills: `dsh-skill-filesystem` scans `<projectRoot>/.dsh/skills` (rank 100),
|
|
44068
44855
|
* `<projectRoot>/.agents/skills` (200), `<dshHome>/skills` (400) and
|
|
@@ -46406,6 +47193,20 @@ function normalizeKimiCodeTransport({ transport, hasCommand }) {
|
|
|
46406
47193
|
if (transport === "http" || transport === "streamable-http") return "http";
|
|
46407
47194
|
return hasCommand ? "stdio" : "http";
|
|
46408
47195
|
}
|
|
47196
|
+
/**
|
|
47197
|
+
* The only `executor` value Kimi Code accepts on a stdio server. 2.2.0 removed
|
|
47198
|
+
* the never-implemented `kaos` option and narrowed the schema to
|
|
47199
|
+
* `z.enum(['local'])`, so any other value makes Kimi fail validation when it
|
|
47200
|
+
* loads `mcp.json`.
|
|
47201
|
+
* @see https://github.com/MoonshotAI/kimi-code/pull/4184
|
|
47202
|
+
*/
|
|
47203
|
+
const KIMI_CODE_MCP_EXECUTOR = "local";
|
|
47204
|
+
/** Remove, with a warning, an `executor` value Kimi Code would reject. Mutates `server`. */
|
|
47205
|
+
function dropUnsupportedKimiCodeExecutor({ name, server, logger }) {
|
|
47206
|
+
if (server.executor === void 0 || server.executor === KIMI_CODE_MCP_EXECUTOR) return;
|
|
47207
|
+
warnWithFallback(logger, `Kimi Code MCP: dropping executor ${JSON.stringify(server.executor)} from "${name}" because Kimi Code only accepts "${KIMI_CODE_MCP_EXECUTOR}".`);
|
|
47208
|
+
delete server.executor;
|
|
47209
|
+
}
|
|
46409
47210
|
function toKimiCodeServer({ name, server, logger }) {
|
|
46410
47211
|
const transport = server.transport ?? server.type;
|
|
46411
47212
|
if (transport === "ws") {
|
|
@@ -46444,6 +47245,11 @@ function toKimiCodeServer({ name, server, logger }) {
|
|
|
46444
47245
|
"enabledTools",
|
|
46445
47246
|
"disabledTools"
|
|
46446
47247
|
]) if (server[field] !== void 0) converted[field] = server[field];
|
|
47248
|
+
dropUnsupportedKimiCodeExecutor({
|
|
47249
|
+
name,
|
|
47250
|
+
server: converted,
|
|
47251
|
+
logger
|
|
47252
|
+
});
|
|
46447
47253
|
if (server.disabled === true) converted.enabled = false;
|
|
46448
47254
|
return converted;
|
|
46449
47255
|
}
|
|
@@ -46459,9 +47265,14 @@ function toKimiCodeServers({ servers, logger }) {
|
|
|
46459
47265
|
}
|
|
46460
47266
|
return result;
|
|
46461
47267
|
}
|
|
46462
|
-
function fromKimiCodeServers(servers) {
|
|
47268
|
+
function fromKimiCodeServers({ servers, logger }) {
|
|
46463
47269
|
return Object.fromEntries(Object.entries(servers).map(([name, server]) => {
|
|
46464
47270
|
const { transport, enabled, ...rest } = server;
|
|
47271
|
+
dropUnsupportedKimiCodeExecutor({
|
|
47272
|
+
name,
|
|
47273
|
+
server: rest,
|
|
47274
|
+
logger
|
|
47275
|
+
});
|
|
46465
47276
|
const type = transport === "stdio" || transport === "sse" || transport === "http" ? transport : void 0;
|
|
46466
47277
|
return [name, {
|
|
46467
47278
|
...rest,
|
|
@@ -46576,9 +47387,11 @@ var KimiCodeMcp = class KimiCodeMcp extends ToolMcp {
|
|
|
46576
47387
|
* a different file that only the async `fromFile` can read.
|
|
46577
47388
|
*/
|
|
46578
47389
|
configDefaults;
|
|
46579
|
-
|
|
47390
|
+
logger;
|
|
47391
|
+
constructor({ configDefaults, logger, ...params }) {
|
|
46580
47392
|
super(params);
|
|
46581
|
-
this.configDefaults =
|
|
47393
|
+
this.configDefaults = configDefaults ?? {};
|
|
47394
|
+
this.logger = logger;
|
|
46582
47395
|
try {
|
|
46583
47396
|
this.json = this.fileContent ? JSON.parse(this.fileContent) : {};
|
|
46584
47397
|
} catch (error) {
|
|
@@ -46594,7 +47407,7 @@ var KimiCodeMcp = class KimiCodeMcp extends ToolMcp {
|
|
|
46594
47407
|
relativeFilePath: KIMI_CODE_MCP_FILE_NAME
|
|
46595
47408
|
};
|
|
46596
47409
|
}
|
|
46597
|
-
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
47410
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false, logger }) {
|
|
46598
47411
|
const paths = this.getSettablePaths({ global });
|
|
46599
47412
|
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"mcpServers\":{}}";
|
|
46600
47413
|
return new KimiCodeMcp({
|
|
@@ -46604,7 +47417,8 @@ var KimiCodeMcp = class KimiCodeMcp extends ToolMcp {
|
|
|
46604
47417
|
fileContent,
|
|
46605
47418
|
validate,
|
|
46606
47419
|
global,
|
|
46607
|
-
configDefaults: global ? await readKimiCodeMcpDefaults({ outputRoot }) : {}
|
|
47420
|
+
configDefaults: global ? await readKimiCodeMcpDefaults({ outputRoot }) : {},
|
|
47421
|
+
logger
|
|
46608
47422
|
});
|
|
46609
47423
|
}
|
|
46610
47424
|
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
@@ -46678,7 +47492,10 @@ var KimiCodeMcp = class KimiCodeMcp extends ToolMcp {
|
|
|
46678
47492
|
relativeFilePath: RULESYNC_MCP_FILE_NAME,
|
|
46679
47493
|
fileContent: JSON.stringify({
|
|
46680
47494
|
...this.json,
|
|
46681
|
-
mcpServers: fromKimiCodeServers(
|
|
47495
|
+
mcpServers: fromKimiCodeServers({
|
|
47496
|
+
servers: isMcpServers(this.json.mcpServers) ? this.json.mcpServers : {},
|
|
47497
|
+
logger: this.logger
|
|
47498
|
+
}),
|
|
46682
47499
|
...Object.keys(this.configDefaults).length > 0 && { "kimi-code": this.configDefaults }
|
|
46683
47500
|
}, null, 2)
|
|
46684
47501
|
});
|
|
@@ -47119,7 +47936,13 @@ var OpencodeMcp = class extends ToolMcp {
|
|
|
47119
47936
|
fileContent = await readFileContentOrNull(jsonPath);
|
|
47120
47937
|
if (fileContent) relativeFilePath = this.layout.jsonFileName;
|
|
47121
47938
|
}
|
|
47122
|
-
const
|
|
47939
|
+
const fileContentToUse = fileContent ?? "{\"mcp\":{}}";
|
|
47940
|
+
const overlays = await readOpencodeDotDirConfigLayers({
|
|
47941
|
+
outputRoot,
|
|
47942
|
+
global,
|
|
47943
|
+
layout: this.layout
|
|
47944
|
+
});
|
|
47945
|
+
const json = overlays.length === 0 ? (0, jsonc_parser.parse)(fileContentToUse) : [(0, jsonc_parser.parse)(fileContentToUse), ...overlays.map(({ config }) => config)].map((layer) => this.lowerConfig(layer, logger)).reduce(mergeOpencodeConfigValues);
|
|
47123
47946
|
const newJson = {
|
|
47124
47947
|
...json,
|
|
47125
47948
|
mcp: json.mcp ?? {}
|
|
@@ -47156,6 +47979,27 @@ var OpencodeMcp = class extends ToolMcp {
|
|
|
47156
47979
|
if (containers.length > 0) logger?.warn(`OpenCode MCP: the whole "mcp" key is rewritten in the flat V1 shape, so ${containers.map(quoteValueForWarning).join(" and ")} ${containers.length === 1 ? "is" : "are"} not kept.`);
|
|
47157
47980
|
}
|
|
47158
47981
|
const { mcp: convertedMcp, tools: mcpTools } = convertToOpencodeFormat(transformedServers, (existingMcp.success ? existingMcp.data.mcp : void 0) ?? {}, logger);
|
|
47982
|
+
const generatedMcp = undefinedWhenEmpty(convertedMcp);
|
|
47983
|
+
const generatedTools = Object.keys(mcpTools).length > 0 ? mcpTools : void 0;
|
|
47984
|
+
warnOpencodeDotDirOverrides({
|
|
47985
|
+
generated: {
|
|
47986
|
+
mcp: generatedMcp,
|
|
47987
|
+
tools: generatedTools
|
|
47988
|
+
},
|
|
47989
|
+
overlays: (await readOpencodeDotDirConfigLayers({
|
|
47990
|
+
outputRoot,
|
|
47991
|
+
global,
|
|
47992
|
+
layout: this.layout
|
|
47993
|
+
})).map((overlay) => {
|
|
47994
|
+
const lowered = this.lowerConfig(overlay.config);
|
|
47995
|
+
return {
|
|
47996
|
+
...overlay,
|
|
47997
|
+
config: isRecord$1(lowered) ? lowered : {}
|
|
47998
|
+
};
|
|
47999
|
+
}),
|
|
48000
|
+
rootFileName: relativeFilePath,
|
|
48001
|
+
logger
|
|
48002
|
+
});
|
|
47159
48003
|
return new this({
|
|
47160
48004
|
outputRoot,
|
|
47161
48005
|
relativeDirPath: basePaths.relativeDirPath,
|
|
@@ -47165,8 +48009,8 @@ var OpencodeMcp = class extends ToolMcp {
|
|
|
47165
48009
|
feature: "mcp",
|
|
47166
48010
|
existingContent: fileContent ?? "",
|
|
47167
48011
|
patch: {
|
|
47168
|
-
mcp:
|
|
47169
|
-
tools:
|
|
48012
|
+
mcp: generatedMcp,
|
|
48013
|
+
tools: generatedTools
|
|
47170
48014
|
},
|
|
47171
48015
|
filePath: (0, node_path.join)(jsonDir, relativeFilePath),
|
|
47172
48016
|
logger
|
|
@@ -47812,6 +48656,358 @@ var OmpMcp = class OmpMcp extends ToolMcp {
|
|
|
47812
48656
|
}
|
|
47813
48657
|
};
|
|
47814
48658
|
//#endregion
|
|
48659
|
+
//#region src/constants/openclaw-paths.ts
|
|
48660
|
+
const OPENCLAW_WORKSPACE_DIR_PATH = (0, node_path.join)(".openclaw", "workspace");
|
|
48661
|
+
const OPENCLAW_RULE_FILE_NAME = "AGENTS.md";
|
|
48662
|
+
const OPENCLAW_CONFIG_DIR_PATH = ".openclaw";
|
|
48663
|
+
const OPENCLAW_CONFIG_FILE_NAME = "openclaw.json";
|
|
48664
|
+
const OPENCLAW_MCP_SERVERS_KEY = "servers";
|
|
48665
|
+
//#endregion
|
|
48666
|
+
//#region src/features/mcp/openclaw-mcp.ts
|
|
48667
|
+
const OPENCLAW_GLOBAL_ONLY_MESSAGE = "OpenClaw MCP is global-only; use --global to sync ~/.openclaw/openclaw.json";
|
|
48668
|
+
/**
|
|
48669
|
+
* Single spelling of the openclaw.json codec/policy, matching its
|
|
48670
|
+
* `SHARED_CONFIG_OWNERSHIP` declaration: the file is JSON5, and a file whose
|
|
48671
|
+
* root is not an object is refused rather than replaced, since it also holds
|
|
48672
|
+
* every other Gateway setting.
|
|
48673
|
+
*/
|
|
48674
|
+
function parseOpenclawConfig(fileContent, filePath) {
|
|
48675
|
+
return parseSharedConfig({
|
|
48676
|
+
format: "json5",
|
|
48677
|
+
fileContent,
|
|
48678
|
+
filePath,
|
|
48679
|
+
invalidRootPolicy: "error"
|
|
48680
|
+
});
|
|
48681
|
+
}
|
|
48682
|
+
const OPENCLAW_OAUTH_STRING_KEYS = [
|
|
48683
|
+
"authProfileId",
|
|
48684
|
+
"scope",
|
|
48685
|
+
"redirectUrl",
|
|
48686
|
+
"clientMetadataUrl"
|
|
48687
|
+
];
|
|
48688
|
+
const OPENCLAW_OAUTH_IDENTITIES = /* @__PURE__ */ new Set(["shared", "per-requester"]);
|
|
48689
|
+
/**
|
|
48690
|
+
* Copies the `oauth` keys OpenClaw accepts. A known key with a value its
|
|
48691
|
+
* schema rejects (an `identity` other than `shared` / `per-requester`, a blank
|
|
48692
|
+
* string) is dropped with a warning, since the Gateway would refuse to start
|
|
48693
|
+
* on it; keys OpenClaw does not know are left behind silently, as they belong
|
|
48694
|
+
* to other tools' `oauth` blocks.
|
|
48695
|
+
*/
|
|
48696
|
+
function copyOpenclawOauth({ source, serverName, logger }) {
|
|
48697
|
+
const oauth = {};
|
|
48698
|
+
const dropped = [];
|
|
48699
|
+
if (source.identity !== void 0) {
|
|
48700
|
+
if (typeof source.identity === "string" && OPENCLAW_OAUTH_IDENTITIES.has(source.identity)) oauth.identity = source.identity;
|
|
48701
|
+
else dropped.push("identity");
|
|
48702
|
+
}
|
|
48703
|
+
for (const key of OPENCLAW_OAUTH_STRING_KEYS) {
|
|
48704
|
+
const value = source[key];
|
|
48705
|
+
if (value === void 0) continue;
|
|
48706
|
+
if (typeof value === "string" && value.trim() !== "") oauth[key] = value;
|
|
48707
|
+
else dropped.push(key);
|
|
48708
|
+
}
|
|
48709
|
+
if (dropped.length > 0 && serverName !== void 0) logger?.warn(`OpenClaw MCP: dropping invalid oauth ${dropped.map((key) => `"${key}"`).join(", ")} of ${quoteValueForWarning(serverName)}; OpenClaw's config validation would reject it.`);
|
|
48710
|
+
return Object.keys(oauth).length > 0 ? oauth : void 0;
|
|
48711
|
+
}
|
|
48712
|
+
const OPENCLAW_CODEX_APPROVAL_MODES = /* @__PURE__ */ new Set([
|
|
48713
|
+
"auto",
|
|
48714
|
+
"prompt",
|
|
48715
|
+
"approve"
|
|
48716
|
+
]);
|
|
48717
|
+
/**
|
|
48718
|
+
* Copies the OpenClaw-only per-server fields that have no canonical
|
|
48719
|
+
* counterpart — `auth`, `oauth`, the TLS controls, the timeouts in
|
|
48720
|
+
* milliseconds, `supportsParallelToolCalls` and the `codex` projection — from
|
|
48721
|
+
* `source` to `target`, in both directions. OpenClaw validates its config
|
|
48722
|
+
* strictly (`oauth`, `toolFilter` and `codex` are strict objects) and refuses
|
|
48723
|
+
* to start the Gateway on a value it rejects, so only the documented keys with
|
|
48724
|
+
* the documented value types are copied; a same-named key another tool reads
|
|
48725
|
+
* differently (Hermes' snake_case `oauth` keys, say) is left behind.
|
|
48726
|
+
*
|
|
48727
|
+
* @see https://github.com/openclaw/openclaw/blob/v2026.10.4/src/config/zod-schema.mcp-server.ts
|
|
48728
|
+
*/
|
|
48729
|
+
function copyOpenclawOnlyFields(source, target, { serverName, logger } = {}) {
|
|
48730
|
+
let copied = false;
|
|
48731
|
+
if (source.auth === "oauth") {
|
|
48732
|
+
target.auth = "oauth";
|
|
48733
|
+
copied = true;
|
|
48734
|
+
}
|
|
48735
|
+
if (isRecord$1(source.oauth)) {
|
|
48736
|
+
const oauth = copyOpenclawOauth({
|
|
48737
|
+
source: source.oauth,
|
|
48738
|
+
serverName,
|
|
48739
|
+
logger
|
|
48740
|
+
});
|
|
48741
|
+
if (oauth) {
|
|
48742
|
+
target.oauth = oauth;
|
|
48743
|
+
copied = true;
|
|
48744
|
+
}
|
|
48745
|
+
}
|
|
48746
|
+
for (const key of ["clientCert", "clientKey"]) if (typeof source[key] === "string") {
|
|
48747
|
+
target[key] = source[key];
|
|
48748
|
+
copied = true;
|
|
48749
|
+
}
|
|
48750
|
+
for (const key of ["sslVerify", "supportsParallelToolCalls"]) if (typeof source[key] === "boolean") {
|
|
48751
|
+
target[key] = source[key];
|
|
48752
|
+
copied = true;
|
|
48753
|
+
}
|
|
48754
|
+
for (const key of ["requestTimeoutMs", "connectionTimeoutMs"]) {
|
|
48755
|
+
const value = source[key];
|
|
48756
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
48757
|
+
target[key] = value;
|
|
48758
|
+
copied = true;
|
|
48759
|
+
}
|
|
48760
|
+
}
|
|
48761
|
+
if (isRecord$1(source.codex)) {
|
|
48762
|
+
const codex = {};
|
|
48763
|
+
if (isStringArray$1(source.codex.agents) && source.codex.agents.length > 0) codex.agents = [...source.codex.agents];
|
|
48764
|
+
const mode = source.codex.defaultToolsApprovalMode;
|
|
48765
|
+
if (typeof mode === "string" && OPENCLAW_CODEX_APPROVAL_MODES.has(mode)) codex.defaultToolsApprovalMode = mode;
|
|
48766
|
+
if (Object.keys(codex).length > 0) {
|
|
48767
|
+
target.codex = codex;
|
|
48768
|
+
copied = true;
|
|
48769
|
+
}
|
|
48770
|
+
}
|
|
48771
|
+
return copied;
|
|
48772
|
+
}
|
|
48773
|
+
/** A string map without pollution keys, or `undefined` when nothing is left. */
|
|
48774
|
+
function copyStringMap(value) {
|
|
48775
|
+
if (!isPlainObject$1(value)) return void 0;
|
|
48776
|
+
const result = {};
|
|
48777
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
48778
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
48779
|
+
if (typeof entry === "string") result[key] = entry;
|
|
48780
|
+
else if (typeof entry === "number" || typeof entry === "boolean") result[key] = String(entry);
|
|
48781
|
+
}
|
|
48782
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
48783
|
+
}
|
|
48784
|
+
/** A non-empty `toolFilter` list, trimmed of blank entries OpenClaw rejects. */
|
|
48785
|
+
function toolFilterList(value) {
|
|
48786
|
+
if (!isStringArray$1(value)) return void 0;
|
|
48787
|
+
const list = value.map((entry) => entry.trim()).filter((entry) => entry !== "");
|
|
48788
|
+
return list.length > 0 ? list : void 0;
|
|
48789
|
+
}
|
|
48790
|
+
function convertServerToOpenclaw({ name, config, logger }) {
|
|
48791
|
+
if (declaresNoTransport(config)) return warnAndSkipMcpServer({
|
|
48792
|
+
toolName: "OpenClaw",
|
|
48793
|
+
serverName: name,
|
|
48794
|
+
reason: "no transport",
|
|
48795
|
+
logger
|
|
48796
|
+
});
|
|
48797
|
+
const out = {};
|
|
48798
|
+
if (isRemoteMcpServer(config)) {
|
|
48799
|
+
const url = resolveRemoteMcpUrl(config);
|
|
48800
|
+
if (!url) return warnAndSkipMcpServer({
|
|
48801
|
+
toolName: "OpenClaw",
|
|
48802
|
+
serverName: name,
|
|
48803
|
+
reason: "a remote transport without a url",
|
|
48804
|
+
logger
|
|
48805
|
+
});
|
|
48806
|
+
const stated = config.type ?? config.transport;
|
|
48807
|
+
if (stated === "ws" || stated === void 0 && /^wss?:\/\//i.test(url)) return warnAndSkipMcpServer({
|
|
48808
|
+
toolName: "OpenClaw",
|
|
48809
|
+
serverName: name,
|
|
48810
|
+
reason: "a WebSocket transport, which OpenClaw does not offer (only streamable-http and sse)",
|
|
48811
|
+
logger
|
|
48812
|
+
});
|
|
48813
|
+
out.url = url;
|
|
48814
|
+
out.transport = stated === "sse" ? "sse" : "streamable-http";
|
|
48815
|
+
const headers = copyStringMap(config.headers);
|
|
48816
|
+
if (headers) out.headers = headers;
|
|
48817
|
+
} else {
|
|
48818
|
+
const [command, ...args] = resolveLocalMcpCommand(config);
|
|
48819
|
+
if (!command) return warnAndSkipMcpServer({
|
|
48820
|
+
toolName: "OpenClaw",
|
|
48821
|
+
serverName: name,
|
|
48822
|
+
reason: "a stdio transport without a command",
|
|
48823
|
+
logger
|
|
48824
|
+
});
|
|
48825
|
+
out.command = command;
|
|
48826
|
+
if (args.length > 0) out.args = args;
|
|
48827
|
+
const env = copyStringMap(config.env);
|
|
48828
|
+
if (env) out.env = env;
|
|
48829
|
+
if (typeof config.cwd === "string") out.cwd = config.cwd;
|
|
48830
|
+
}
|
|
48831
|
+
if (config.disabled === true) out.enabled = false;
|
|
48832
|
+
const include = toolFilterList(config.enabledTools);
|
|
48833
|
+
const exclude = toolFilterList(config.disabledTools);
|
|
48834
|
+
if (include || exclude) out.toolFilter = {
|
|
48835
|
+
...include && { include },
|
|
48836
|
+
...exclude && { exclude }
|
|
48837
|
+
};
|
|
48838
|
+
copyOpenclawOnlyFields(config, out, {
|
|
48839
|
+
serverName: name,
|
|
48840
|
+
logger
|
|
48841
|
+
});
|
|
48842
|
+
return out;
|
|
48843
|
+
}
|
|
48844
|
+
/**
|
|
48845
|
+
* Converts canonical rulesync servers into OpenClaw `mcp.servers` entries.
|
|
48846
|
+
* Only keys OpenClaw accepts are written — never canonical-only ones such as
|
|
48847
|
+
* `type` or `disabled`, which its strict config validation would answer by
|
|
48848
|
+
* refusing to start the Gateway.
|
|
48849
|
+
*/
|
|
48850
|
+
function convertToOpenclawFormat({ mcpServers, logger }) {
|
|
48851
|
+
const result = {};
|
|
48852
|
+
for (const [name, config] of Object.entries(mcpServers)) {
|
|
48853
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
|
|
48854
|
+
const converted = convertServerToOpenclaw({
|
|
48855
|
+
name,
|
|
48856
|
+
config,
|
|
48857
|
+
logger
|
|
48858
|
+
});
|
|
48859
|
+
if (converted) result[name] = converted;
|
|
48860
|
+
}
|
|
48861
|
+
return result;
|
|
48862
|
+
}
|
|
48863
|
+
/**
|
|
48864
|
+
* Converts OpenClaw `mcp.servers` entries back into canonical rulesync
|
|
48865
|
+
* servers. OpenClaw-only fields go into the `openclaw.mcpServers` override
|
|
48866
|
+
* block (the whole server, since an override entry replaces the shared one for
|
|
48867
|
+
* that tool), so they survive an import/generate round trip without leaking
|
|
48868
|
+
* into every other tool's config.
|
|
48869
|
+
*/
|
|
48870
|
+
function convertFromOpenclawFormat(servers) {
|
|
48871
|
+
const mcpServers = {};
|
|
48872
|
+
const openclawOverrides = {};
|
|
48873
|
+
for (const [name, config] of Object.entries(servers)) {
|
|
48874
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
|
|
48875
|
+
const server = {};
|
|
48876
|
+
if (typeof config.command === "string") {
|
|
48877
|
+
server.command = config.command;
|
|
48878
|
+
if (isStringArray$1(config.args)) server.args = config.args;
|
|
48879
|
+
const env = copyStringMap(config.env);
|
|
48880
|
+
if (env) server.env = env;
|
|
48881
|
+
if (typeof config.cwd === "string") server.cwd = config.cwd;
|
|
48882
|
+
} else if (typeof config.url === "string") {
|
|
48883
|
+
server.type = config.transport === "sse" ? "sse" : "http";
|
|
48884
|
+
server.url = config.url;
|
|
48885
|
+
const headers = copyStringMap(config.headers);
|
|
48886
|
+
if (headers) server.headers = headers;
|
|
48887
|
+
}
|
|
48888
|
+
if (config.enabled === false) server.disabled = true;
|
|
48889
|
+
if (isRecord$1(config.toolFilter)) {
|
|
48890
|
+
const include = toolFilterList(config.toolFilter.include);
|
|
48891
|
+
const exclude = toolFilterList(config.toolFilter.exclude);
|
|
48892
|
+
if (include) server.enabledTools = include;
|
|
48893
|
+
if (exclude) server.disabledTools = exclude;
|
|
48894
|
+
}
|
|
48895
|
+
mcpServers[name] = server;
|
|
48896
|
+
const openclawServer = { ...server };
|
|
48897
|
+
if (copyOpenclawOnlyFields(config, openclawServer)) openclawOverrides[name] = openclawServer;
|
|
48898
|
+
}
|
|
48899
|
+
return {
|
|
48900
|
+
mcpServers,
|
|
48901
|
+
openclawOverrides
|
|
48902
|
+
};
|
|
48903
|
+
}
|
|
48904
|
+
/**
|
|
48905
|
+
* OpenClaw MCP servers.
|
|
48906
|
+
*
|
|
48907
|
+
* OpenClaw keeps its MCP registry under `mcp.servers` of the Gateway config
|
|
48908
|
+
* `~/.openclaw/openclaw.json` (JSON5; global only — there is no project-scoped
|
|
48909
|
+
* MCP location). That file also holds credentials and every other Gateway
|
|
48910
|
+
* setting, so generation merges rulesync's servers into `mcp.servers` by name,
|
|
48911
|
+
* keeps servers rulesync does not define and every other key, and never deletes
|
|
48912
|
+
* the file.
|
|
48913
|
+
*
|
|
48914
|
+
* @see https://docs.openclaw.ai/gateway/config-extensions
|
|
48915
|
+
* @see https://docs.openclaw.ai/tools/mcp
|
|
48916
|
+
*/
|
|
48917
|
+
var OpenclawMcp = class OpenclawMcp extends ToolMcp {
|
|
48918
|
+
config;
|
|
48919
|
+
constructor(params) {
|
|
48920
|
+
super(params);
|
|
48921
|
+
this.config = parseOpenclawConfig(this.fileContent ?? "", (0, node_path.join)(this.relativeDirPath, this.relativeFilePath));
|
|
48922
|
+
}
|
|
48923
|
+
getConfig() {
|
|
48924
|
+
return this.config;
|
|
48925
|
+
}
|
|
48926
|
+
isDeletable() {
|
|
48927
|
+
return false;
|
|
48928
|
+
}
|
|
48929
|
+
static getSettablePaths(_options = {}) {
|
|
48930
|
+
return {
|
|
48931
|
+
relativeDirPath: OPENCLAW_CONFIG_DIR_PATH,
|
|
48932
|
+
relativeFilePath: OPENCLAW_CONFIG_FILE_NAME
|
|
48933
|
+
};
|
|
48934
|
+
}
|
|
48935
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
48936
|
+
if (!global) throw new Error(OPENCLAW_GLOBAL_ONLY_MESSAGE);
|
|
48937
|
+
const paths = this.getSettablePaths({ global });
|
|
48938
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
|
|
48939
|
+
return new OpenclawMcp({
|
|
48940
|
+
outputRoot,
|
|
48941
|
+
relativeDirPath: paths.relativeDirPath,
|
|
48942
|
+
relativeFilePath: paths.relativeFilePath,
|
|
48943
|
+
fileContent,
|
|
48944
|
+
validate,
|
|
48945
|
+
global
|
|
48946
|
+
});
|
|
48947
|
+
}
|
|
48948
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
48949
|
+
if (!global) throw new Error(OPENCLAW_GLOBAL_ONLY_MESSAGE);
|
|
48950
|
+
const paths = this.getSettablePaths({ global });
|
|
48951
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
48952
|
+
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
48953
|
+
const existing = parseOpenclawConfig(existingContent, filePath);
|
|
48954
|
+
const existingMcp = isRecord$1(existing["mcp"]) ? existing["mcp"] : {};
|
|
48955
|
+
const existingServers = isRecord$1(existingMcp["servers"]) ? existingMcp[OPENCLAW_MCP_SERVERS_KEY] : {};
|
|
48956
|
+
const converted = convertToOpenclawFormat({
|
|
48957
|
+
mcpServers: rulesyncMcp.getMcpServers(),
|
|
48958
|
+
logger
|
|
48959
|
+
});
|
|
48960
|
+
const servers = {
|
|
48961
|
+
...existingServers,
|
|
48962
|
+
...converted
|
|
48963
|
+
};
|
|
48964
|
+
const kept = Object.keys(existingServers).filter((name) => !PROTOTYPE_POLLUTION_KEYS.has(name) && !Object.hasOwn(converted, name));
|
|
48965
|
+
if (kept.length > 0) logger?.warn(`OpenClaw MCP servers not defined in rulesync are kept in ${(0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath)}: ${kept.map((name) => quoteValueForWarning(name)).join(", ")}. Remove them with 'openclaw mcp unset <name>' if they are no longer wanted.`);
|
|
48966
|
+
return new OpenclawMcp({
|
|
48967
|
+
outputRoot,
|
|
48968
|
+
relativeDirPath: paths.relativeDirPath,
|
|
48969
|
+
relativeFilePath: paths.relativeFilePath,
|
|
48970
|
+
fileContent: applySharedConfigPatch({
|
|
48971
|
+
fileKey: sharedConfigFileKey(paths),
|
|
48972
|
+
feature: "mcp",
|
|
48973
|
+
existingContent,
|
|
48974
|
+
patch: { ["mcp"]: {
|
|
48975
|
+
...existingMcp,
|
|
48976
|
+
[OPENCLAW_MCP_SERVERS_KEY]: servers
|
|
48977
|
+
} },
|
|
48978
|
+
filePath,
|
|
48979
|
+
logger
|
|
48980
|
+
}),
|
|
48981
|
+
validate,
|
|
48982
|
+
global
|
|
48983
|
+
});
|
|
48984
|
+
}
|
|
48985
|
+
toRulesyncMcp() {
|
|
48986
|
+
const mcp = isRecord$1(this.config["mcp"]) ? this.config["mcp"] : {};
|
|
48987
|
+
const { mcpServers, openclawOverrides } = convertFromOpenclawFormat(isRecord$1(mcp["servers"]) ? mcp[OPENCLAW_MCP_SERVERS_KEY] : {});
|
|
48988
|
+
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({
|
|
48989
|
+
mcpServers,
|
|
48990
|
+
...Object.keys(openclawOverrides).length > 0 && { openclaw: { mcpServers: openclawOverrides } }
|
|
48991
|
+
}, null, 2) });
|
|
48992
|
+
}
|
|
48993
|
+
validate() {
|
|
48994
|
+
return {
|
|
48995
|
+
success: true,
|
|
48996
|
+
error: null
|
|
48997
|
+
};
|
|
48998
|
+
}
|
|
48999
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
49000
|
+
return new OpenclawMcp({
|
|
49001
|
+
outputRoot,
|
|
49002
|
+
relativeDirPath,
|
|
49003
|
+
relativeFilePath,
|
|
49004
|
+
fileContent: "",
|
|
49005
|
+
validate: false,
|
|
49006
|
+
global
|
|
49007
|
+
});
|
|
49008
|
+
}
|
|
49009
|
+
};
|
|
49010
|
+
//#endregion
|
|
47815
49011
|
//#region src/features/mcp/pi-mcp.ts
|
|
47816
49012
|
/**
|
|
47817
49013
|
* Canonical keys consumed by the conversion below (or with no Pi counterpart),
|
|
@@ -50866,6 +52062,128 @@ function asZcodeRemoteType(stated, url) {
|
|
|
50866
52062
|
if (stated === void 0) return /^wss?:\/\//i.test(url) ? void 0 : "http";
|
|
50867
52063
|
}
|
|
50868
52064
|
/**
|
|
52065
|
+
* The keys each OAuth flow of ZCode's per-server `oauth` union accepts on a
|
|
52066
|
+
* remote server, and which of them are required. Read off the 3.14.3/3.15.3
|
|
52067
|
+
* runtime config schema, since ZCode's MCP page only says to "enable OAuth for
|
|
52068
|
+
* the service in its configuration". Both variants are strict, so any other
|
|
52069
|
+
* key would make ZCode skip the server.
|
|
52070
|
+
*/
|
|
52071
|
+
const ZCODE_OAUTH_FLOWS = {
|
|
52072
|
+
client_credentials: {
|
|
52073
|
+
required: ["clientId", "clientSecret"],
|
|
52074
|
+
optional: ["clientName", "scope"]
|
|
52075
|
+
},
|
|
52076
|
+
authorization_code: {
|
|
52077
|
+
required: [],
|
|
52078
|
+
optional: [
|
|
52079
|
+
"clientId",
|
|
52080
|
+
"clientSecret",
|
|
52081
|
+
"clientName",
|
|
52082
|
+
"redirectPath",
|
|
52083
|
+
"scope"
|
|
52084
|
+
]
|
|
52085
|
+
}
|
|
52086
|
+
};
|
|
52087
|
+
/**
|
|
52088
|
+
* The `protocolVersion` values ZCode's server schema accepts (an enum on a
|
|
52089
|
+
* strict object, so any other value makes ZCode skip the server).
|
|
52090
|
+
*/
|
|
52091
|
+
const ZCODE_PROTOCOL_VERSIONS = /* @__PURE__ */ new Set([
|
|
52092
|
+
"auto",
|
|
52093
|
+
"legacy",
|
|
52094
|
+
"2026-07-28"
|
|
52095
|
+
]);
|
|
52096
|
+
/**
|
|
52097
|
+
* The namespaced canonical key that carries ZCode's `oauth`. The canonical
|
|
52098
|
+
* `oauth` already holds Claude Code's `{ clientId }` object, and ZCode's shape
|
|
52099
|
+
* may carry a client secret, so it is kept apart (the `crushOauth*`
|
|
52100
|
+
* precedent) and stripped by `RulesyncMcp.getMcpServers()`.
|
|
52101
|
+
*/
|
|
52102
|
+
const ZCODE_OAUTH_CANONICAL_KEY = "zcodeOauth";
|
|
52103
|
+
/**
|
|
52104
|
+
* Whether an `oauth` value names one of ZCode's flows.
|
|
52105
|
+
*/
|
|
52106
|
+
function isZcodeOauth(value) {
|
|
52107
|
+
return isRecord$1(value) && typeof value.type === "string" && value.type in ZCODE_OAUTH_FLOWS;
|
|
52108
|
+
}
|
|
52109
|
+
/**
|
|
52110
|
+
* Build ZCode's `oauth` from a `zcodeOauth` value: only the keys of the named
|
|
52111
|
+
* flow are kept, as strings. A value naming no ZCode flow, or missing a key
|
|
52112
|
+
* the flow requires, is dropped with a warning instead of making ZCode skip
|
|
52113
|
+
* the server.
|
|
52114
|
+
*/
|
|
52115
|
+
function toZcodeOauth({ serverName, value, logger }) {
|
|
52116
|
+
if (value === void 0) return void 0;
|
|
52117
|
+
if (!isZcodeOauth(value)) {
|
|
52118
|
+
logger?.warn(`ZCode MCP server "${serverName}": dropping zcodeOauth, as its type must be "client_credentials" or "authorization_code".`);
|
|
52119
|
+
return;
|
|
52120
|
+
}
|
|
52121
|
+
const type = String(value.type);
|
|
52122
|
+
const flow = ZCODE_OAUTH_FLOWS[type];
|
|
52123
|
+
if (!flow) return void 0;
|
|
52124
|
+
const missing = flow.required.filter((key) => typeof value[key] !== "string");
|
|
52125
|
+
if (missing.length > 0) {
|
|
52126
|
+
logger?.warn(`ZCode MCP server "${serverName}": dropping zcodeOauth, as the ${type} flow requires ${missing.join(", ")}.`);
|
|
52127
|
+
return;
|
|
52128
|
+
}
|
|
52129
|
+
const allowed = /* @__PURE__ */ new Set([...flow.required, ...flow.optional]);
|
|
52130
|
+
const oauth = { type };
|
|
52131
|
+
const ignored = [];
|
|
52132
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
52133
|
+
if (key === "type" || PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
52134
|
+
if (allowed.has(key) && typeof entry === "string") oauth[key] = entry;
|
|
52135
|
+
else ignored.push(key);
|
|
52136
|
+
}
|
|
52137
|
+
if (ignored.length > 0) logger?.warn(`ZCode MCP server "${serverName}": ignoring zcodeOauth keys the ${type} flow does not accept: ${ignored.join(", ")}.`);
|
|
52138
|
+
return oauth;
|
|
52139
|
+
}
|
|
52140
|
+
/**
|
|
52141
|
+
* The `zcodeOauth` a server was authored with. `getMcpServers()` strips it, so
|
|
52142
|
+
* the authored server (`rawServerOf`) is consulted first.
|
|
52143
|
+
*/
|
|
52144
|
+
function authoredZcodeOauth({ name, config, rawServerOf }) {
|
|
52145
|
+
const raw = rawServerOf?.(name);
|
|
52146
|
+
return isRecord$1(raw) ? raw[ZCODE_OAUTH_CANONICAL_KEY] : config[ZCODE_OAUTH_CANONICAL_KEY];
|
|
52147
|
+
}
|
|
52148
|
+
function isPositiveMs(value) {
|
|
52149
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
52150
|
+
}
|
|
52151
|
+
/**
|
|
52152
|
+
* Resolve ZCode's per-server `timeoutMs` (a positive number of milliseconds):
|
|
52153
|
+
* a valid explicit `timeoutMs` wins, otherwise the canonical `timeout`
|
|
52154
|
+
* (milliseconds, the `gitlabduo-mcp.ts` precedent) is used. A value ZCode
|
|
52155
|
+
* would reject is dropped with a warning instead of making ZCode skip the
|
|
52156
|
+
* server.
|
|
52157
|
+
*/
|
|
52158
|
+
function resolveZcodeTimeoutMs({ serverName, config, logger }) {
|
|
52159
|
+
const { timeoutMs, timeout } = config;
|
|
52160
|
+
if (timeoutMs !== void 0) {
|
|
52161
|
+
if (isPositiveMs(timeoutMs)) return timeoutMs;
|
|
52162
|
+
logger?.warn(`ZCode MCP server "${serverName}": ignoring timeoutMs ${JSON.stringify(timeoutMs)}, as it must be a positive number of milliseconds.`);
|
|
52163
|
+
}
|
|
52164
|
+
if (timeout === void 0) return void 0;
|
|
52165
|
+
if (isPositiveMs(timeout)) return timeout;
|
|
52166
|
+
logger?.warn(`ZCode MCP server "${serverName}": dropping timeout ${JSON.stringify(timeout)}, as timeoutMs must be a positive number of milliseconds.`);
|
|
52167
|
+
}
|
|
52168
|
+
/**
|
|
52169
|
+
* The per-server keys ZCode reads on every transport: `timeoutMs`,
|
|
52170
|
+
* `protocolVersion` and the enable toggle (`enableKey`).
|
|
52171
|
+
*/
|
|
52172
|
+
function toZcodeCommonKeys({ serverName, config, enableKey, logger }) {
|
|
52173
|
+
const common = {};
|
|
52174
|
+
const timeoutMs = resolveZcodeTimeoutMs({
|
|
52175
|
+
serverName,
|
|
52176
|
+
config,
|
|
52177
|
+
logger
|
|
52178
|
+
});
|
|
52179
|
+
if (timeoutMs !== void 0) common.timeoutMs = timeoutMs;
|
|
52180
|
+
const { protocolVersion } = config;
|
|
52181
|
+
if (ZCODE_PROTOCOL_VERSIONS.has(protocolVersion)) common.protocolVersion = protocolVersion;
|
|
52182
|
+
else if (protocolVersion !== void 0) logger?.warn(`ZCode MCP server "${serverName}": dropping protocolVersion ${JSON.stringify(protocolVersion)}, as ZCode accepts only ${[...ZCODE_PROTOCOL_VERSIONS].map((v) => JSON.stringify(v)).join(", ")}.`);
|
|
52183
|
+
if (config.disabled === true) common[enableKey] = false;
|
|
52184
|
+
return common;
|
|
52185
|
+
}
|
|
52186
|
+
/**
|
|
50869
52187
|
* Convert canonical rulesync servers to ZCode's native `mcp.servers` shape:
|
|
50870
52188
|
* stdio servers carry `command`/`args`/`env`, remote servers carry
|
|
50871
52189
|
* `type` (`http` or `sse`), `url` and optional `headers`. A canonical
|
|
@@ -50873,9 +52191,18 @@ function asZcodeRemoteType(stated, url) {
|
|
|
50873
52191
|
* plugin's `.mcp.json`, via `enableKey`), which it defaults to `true` when
|
|
50874
52192
|
* absent.
|
|
50875
52193
|
*
|
|
52194
|
+
* Every server also carries `timeoutMs` (from the canonical `timeout`, or an
|
|
52195
|
+
* explicit `timeoutMs`) and an allow-listed `protocolVersion`; stdio servers
|
|
52196
|
+
* carry `cwd`, and remote servers an `oauth` built from the namespaced
|
|
52197
|
+
* `zcodeOauth`. That key is stripped by `RulesyncMcp.getMcpServers()`, so it
|
|
52198
|
+
* is read from the authored server through `rawServerOf`. ZCode's docs do not
|
|
52199
|
+
* name these keys, so they follow the 3.14.3/3.15.3 runtime's per-server
|
|
52200
|
+
* schema, which is strict: an unknown key makes ZCode skip the server, which
|
|
52201
|
+
* is why the output stays a whitelist.
|
|
52202
|
+
*
|
|
50876
52203
|
* @see https://zcode.z.ai/en/docs/mcp-services
|
|
50877
52204
|
*/
|
|
50878
|
-
function convertToZcodeFormat({ mcpServers, logger, enableKey = "enable" }) {
|
|
52205
|
+
function convertToZcodeFormat({ mcpServers, logger, enableKey = "enable", rawServerOf }) {
|
|
50879
52206
|
const result = {};
|
|
50880
52207
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
50881
52208
|
if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
|
|
@@ -50915,6 +52242,16 @@ function convertToZcodeFormat({ mcpServers, logger, enableKey = "enable" }) {
|
|
|
50915
52242
|
converted.type = type;
|
|
50916
52243
|
converted.url = url;
|
|
50917
52244
|
if (config.headers && Object.keys(config.headers).length > 0) converted.headers = config.headers;
|
|
52245
|
+
const oauth = toZcodeOauth({
|
|
52246
|
+
serverName: name,
|
|
52247
|
+
value: authoredZcodeOauth({
|
|
52248
|
+
name,
|
|
52249
|
+
config,
|
|
52250
|
+
rawServerOf
|
|
52251
|
+
}),
|
|
52252
|
+
logger
|
|
52253
|
+
});
|
|
52254
|
+
if (oauth !== void 0) converted.oauth = oauth;
|
|
50918
52255
|
} else {
|
|
50919
52256
|
const [command, ...args] = resolveLocalMcpCommand(config);
|
|
50920
52257
|
if (!command) {
|
|
@@ -50929,35 +52266,62 @@ function convertToZcodeFormat({ mcpServers, logger, enableKey = "enable" }) {
|
|
|
50929
52266
|
converted.command = command;
|
|
50930
52267
|
converted.args = args;
|
|
50931
52268
|
if (config.env && Object.keys(config.env).length > 0) converted.env = config.env;
|
|
52269
|
+
if (typeof config.cwd === "string" && config.cwd !== "") converted.cwd = config.cwd;
|
|
50932
52270
|
}
|
|
50933
|
-
|
|
50934
|
-
|
|
52271
|
+
result[name] = {
|
|
52272
|
+
...converted,
|
|
52273
|
+
...toZcodeCommonKeys({
|
|
52274
|
+
serverName: name,
|
|
52275
|
+
config,
|
|
52276
|
+
enableKey,
|
|
52277
|
+
logger
|
|
52278
|
+
})
|
|
52279
|
+
};
|
|
50935
52280
|
}
|
|
50936
52281
|
return result;
|
|
50937
52282
|
}
|
|
50938
52283
|
/**
|
|
52284
|
+
* ZCode's per-server enable toggle, in both spellings: the runtime normalizes
|
|
52285
|
+
* the legacy `enable` to `enabled`, and a plugin's `.mcp.json` uses `enabled`.
|
|
52286
|
+
*/
|
|
52287
|
+
const ZCODE_ENABLE_KEYS = /* @__PURE__ */ new Set(["enable", "enabled"]);
|
|
52288
|
+
/**
|
|
50939
52289
|
* Convert ZCode's native `mcp.servers` shape back to canonical rulesync
|
|
50940
|
-
* servers: `enable: false`
|
|
50941
|
-
* through untouched so an import keeps whatever a
|
|
50942
|
-
*
|
|
50943
|
-
*
|
|
52290
|
+
* servers: `enable: false` / `enabled: false` map back to `disabled: true`,
|
|
52291
|
+
* and unknown keys pass through untouched so an import keeps whatever a
|
|
52292
|
+
* hand-authored entry declared. `timeoutMs` is lifted back into the canonical
|
|
52293
|
+
* `timeout` (milliseconds); when the server also states a `timeout`, ZCode
|
|
52294
|
+
* has already dropped that one, so `timeoutMs` replaces it. A ZCode-shaped
|
|
52295
|
+
* `oauth` is imported as the namespaced `zcodeOauth`, so its client secret is
|
|
52296
|
+
* never emitted to another tool. Generation back out is a whitelist, so only
|
|
52297
|
+
* the keys `convertToZcodeFormat` writes are re-emitted.
|
|
50944
52298
|
*
|
|
50945
52299
|
* An explicit `enable: true` is dropped rather than mapped to a canonical
|
|
50946
52300
|
* `disabled: false`: absent already means enabled on both sides, so the flag
|
|
50947
52301
|
* carries no information, and emitting it would push a redundant `disabled`
|
|
50948
|
-
* key into every other tool's generated config.
|
|
52302
|
+
* key into every other tool's generated config. (A canonical `enabled` is
|
|
52303
|
+
* rulesync's own generation filter, so it must never be passed through.)
|
|
50949
52304
|
*/
|
|
50950
|
-
function convertFromZcodeFormat({ zcodeServers
|
|
52305
|
+
function convertFromZcodeFormat({ zcodeServers }) {
|
|
50951
52306
|
const result = {};
|
|
50952
52307
|
for (const [name, config] of Object.entries(zcodeServers)) {
|
|
50953
52308
|
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
|
|
50954
52309
|
const converted = {};
|
|
50955
52310
|
for (const [key, value] of Object.entries(config)) {
|
|
50956
52311
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
50957
|
-
if (key
|
|
52312
|
+
if (ZCODE_ENABLE_KEYS.has(key)) {
|
|
50958
52313
|
if (value === false) converted.disabled = true;
|
|
50959
52314
|
continue;
|
|
50960
52315
|
}
|
|
52316
|
+
if (key === "timeoutMs" && typeof value === "number") {
|
|
52317
|
+
converted.timeout = value;
|
|
52318
|
+
continue;
|
|
52319
|
+
}
|
|
52320
|
+
if (key === "timeout" && typeof config.timeoutMs === "number") continue;
|
|
52321
|
+
if (key === "oauth" && isZcodeOauth(value)) {
|
|
52322
|
+
converted[ZCODE_OAUTH_CANONICAL_KEY] = value;
|
|
52323
|
+
continue;
|
|
52324
|
+
}
|
|
50961
52325
|
converted[key] = value;
|
|
50962
52326
|
}
|
|
50963
52327
|
result[name] = converted;
|
|
@@ -51014,7 +52378,8 @@ var ZcodeMcp = class ZcodeMcp extends ToolMcp {
|
|
|
51014
52378
|
const existing = parseZcodeConfig(existingContent, filePath);
|
|
51015
52379
|
const converted = convertToZcodeFormat({
|
|
51016
52380
|
mcpServers: rulesyncMcp.getMcpServers(),
|
|
51017
|
-
logger
|
|
52381
|
+
logger,
|
|
52382
|
+
rawServerOf: (name) => rulesyncMcp.getRawMcpServer(name)
|
|
51018
52383
|
});
|
|
51019
52384
|
const existingMcp = isRecord$1(existing["mcp"]) ? existing["mcp"] : {};
|
|
51020
52385
|
return new ZcodeMcp({
|
|
@@ -51118,7 +52483,8 @@ var ZcodePluginMcp = class ZcodePluginMcp extends ToolMcp {
|
|
|
51118
52483
|
const mcpServers = convertToZcodeFormat({
|
|
51119
52484
|
mcpServers: rulesyncMcp.getMcpServers(),
|
|
51120
52485
|
logger,
|
|
51121
|
-
enableKey: ZCODE_PLUGIN_ENABLE_KEY
|
|
52486
|
+
enableKey: ZCODE_PLUGIN_ENABLE_KEY,
|
|
52487
|
+
rawServerOf: (name) => rulesyncMcp.getRawMcpServer(name)
|
|
51122
52488
|
});
|
|
51123
52489
|
return new ZcodePluginMcp({
|
|
51124
52490
|
outputRoot,
|
|
@@ -51129,10 +52495,7 @@ var ZcodePluginMcp = class ZcodePluginMcp extends ToolMcp {
|
|
|
51129
52495
|
});
|
|
51130
52496
|
}
|
|
51131
52497
|
toRulesyncMcp() {
|
|
51132
|
-
const mcpServers = convertFromZcodeFormat({
|
|
51133
|
-
zcodeServers: serversOf(this.json),
|
|
51134
|
-
enableKey: ZCODE_PLUGIN_ENABLE_KEY
|
|
51135
|
-
});
|
|
52498
|
+
const mcpServers = convertFromZcodeFormat({ zcodeServers: serversOf(this.json) });
|
|
51136
52499
|
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
|
|
51137
52500
|
}
|
|
51138
52501
|
validate() {
|
|
@@ -51637,6 +53000,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
51637
53000
|
supportsDisabledTools: true
|
|
51638
53001
|
}
|
|
51639
53002
|
}],
|
|
53003
|
+
["openclaw", {
|
|
53004
|
+
class: OpenclawMcp,
|
|
53005
|
+
meta: {
|
|
53006
|
+
supportsProject: false,
|
|
53007
|
+
supportsGlobal: true,
|
|
53008
|
+
supportsEnabledTools: true,
|
|
53009
|
+
supportsDisabledTools: true
|
|
53010
|
+
}
|
|
53011
|
+
}],
|
|
51640
53012
|
["kimi-code", {
|
|
51641
53013
|
class: KimiCodeMcp,
|
|
51642
53014
|
meta: {
|
|
@@ -52790,7 +54162,7 @@ function parsedGlobsIntersect(left, right, budget) {
|
|
|
52790
54162
|
*
|
|
52791
54163
|
* @see https://antigravity.google/docs/permissions?tab=cli
|
|
52792
54164
|
*/
|
|
52793
|
-
const REGEX_PREFIX = "regex:";
|
|
54165
|
+
const REGEX_PREFIX$1 = "regex:";
|
|
52794
54166
|
/**
|
|
52795
54167
|
* The Antigravity actions whose target is a command: `command`, and
|
|
52796
54168
|
* `unsandboxed`, which takes the same word-by-word prefix or `regex:` target.
|
|
@@ -53128,7 +54500,7 @@ function wordToRegexSource(word) {
|
|
|
53128
54500
|
* pattern does.
|
|
53129
54501
|
*/
|
|
53130
54502
|
function toAntigravityCommandTarget(pattern) {
|
|
53131
|
-
if (pattern.startsWith(REGEX_PREFIX)) {
|
|
54503
|
+
if (pattern.startsWith(REGEX_PREFIX$1)) {
|
|
53132
54504
|
const regexWords = splitWords(pattern.slice(6));
|
|
53133
54505
|
if (regexWords.length === 0) return { skipReason: "The regex is empty" };
|
|
53134
54506
|
const invalid = regexWords.find(isInvalidRegex);
|
|
@@ -53148,7 +54520,7 @@ function toAntigravityCommandTarget(pattern) {
|
|
|
53148
54520
|
const head = words.slice(0, -1);
|
|
53149
54521
|
if (head.length > 0 && isStarWord(words.at(-1) ?? []) && head.every(isLiteralWord)) return { target: head.map(wordToLiteral).join(" ") };
|
|
53150
54522
|
return {
|
|
53151
|
-
target: `${REGEX_PREFIX}${words.map(wordToRegexSource).join(" ")}`,
|
|
54523
|
+
target: `${REGEX_PREFIX$1}${words.map(wordToRegexSource).join(" ")}`,
|
|
53152
54524
|
note
|
|
53153
54525
|
};
|
|
53154
54526
|
}
|
|
@@ -53160,7 +54532,7 @@ function toAntigravityCommandTarget(pattern) {
|
|
|
53160
54532
|
* word with a brace may be part of `{a,b}` alternatives, so it ends the prefix.
|
|
53161
54533
|
*/
|
|
53162
54534
|
function toAntigravityCommandLiteralPrefix(pattern) {
|
|
53163
|
-
if (pattern.startsWith(REGEX_PREFIX)) return;
|
|
54535
|
+
if (pattern.startsWith(REGEX_PREFIX$1)) return;
|
|
53164
54536
|
const words = splitIntoWords(parseGlobPattern(pattern).steps);
|
|
53165
54537
|
const prefix = [];
|
|
53166
54538
|
for (const word of words) {
|
|
@@ -53168,7 +54540,7 @@ function toAntigravityCommandLiteralPrefix(pattern) {
|
|
|
53168
54540
|
if (literal === void 0 || literal.includes("{") || literal.includes("}")) break;
|
|
53169
54541
|
prefix.push(literal);
|
|
53170
54542
|
}
|
|
53171
|
-
if (prefix.length === 0 || prefix[0]?.startsWith(REGEX_PREFIX)) return;
|
|
54543
|
+
if (prefix.length === 0 || prefix[0]?.startsWith(REGEX_PREFIX$1)) return;
|
|
53172
54544
|
return prefix.join(" ");
|
|
53173
54545
|
}
|
|
53174
54546
|
/**
|
|
@@ -53219,11 +54591,11 @@ function regexToGlob(source) {
|
|
|
53219
54591
|
* `regex:` target is kept as is.
|
|
53220
54592
|
*/
|
|
53221
54593
|
function fromAntigravityCommandTarget(target) {
|
|
53222
|
-
if (target.startsWith(REGEX_PREFIX)) return regexToGlob(target.slice(6)) ?? target;
|
|
54594
|
+
if (target.startsWith(REGEX_PREFIX$1)) return regexToGlob(target.slice(6)) ?? target;
|
|
53223
54595
|
if (target === "*") return target;
|
|
53224
54596
|
if (PLAIN_TARGET_GLOB_CHARACTERS.test(target)) {
|
|
53225
54597
|
const words = splitWords(target);
|
|
53226
|
-
return `${REGEX_PREFIX}${words.map((word) => `^${[...word].map(escapeRegexCharacter).join("")}$`).join(" ")}`;
|
|
54598
|
+
return `${REGEX_PREFIX$1}${words.map((word) => `^${[...word].map(escapeRegexCharacter).join("")}$`).join(" ")}`;
|
|
53227
54599
|
}
|
|
53228
54600
|
return `${target} *`;
|
|
53229
54601
|
}
|
|
@@ -53243,7 +54615,7 @@ function fromAntigravityCommandTarget(target) {
|
|
|
53243
54615
|
* @see https://antigravity.google/docs/permissions
|
|
53244
54616
|
*/
|
|
53245
54617
|
function describeAntigravityCommandImportLoss(target) {
|
|
53246
|
-
if (target.startsWith(REGEX_PREFIX) || target === "*") return;
|
|
54618
|
+
if (target.startsWith(REGEX_PREFIX$1) || target === "*") return;
|
|
53247
54619
|
if (PLAIN_TARGET_GLOB_CHARACTERS.test(target)) return {
|
|
53248
54620
|
reason: "it holds characters that are special in a glob (`*`, `?`, `[`, `{` or `\\`), which Antigravity matches literally (likely output of an older rulesync), so it is imported as a literal-match regex that no other tool enforces",
|
|
53249
54621
|
denyConsequence: "This deny is NOT enforced by the other tools it is generated for."
|
|
@@ -55145,7 +56517,7 @@ function buildVscodeCommandLists({ rules, toolLabel, logger, prefixSemantics = "
|
|
|
55145
56517
|
//#region src/features/permissions/bob-permissions.ts
|
|
55146
56518
|
const TOOL_LABEL = "IBM Bob";
|
|
55147
56519
|
/** The top-level settings key holding Bob's auto-approval configuration. */
|
|
55148
|
-
const APPROVAL_KEY = "approval";
|
|
56520
|
+
const APPROVAL_KEY$1 = "approval";
|
|
55149
56521
|
/** The top-level settings key holding Bob's per-feature auto-approve toggles. */
|
|
55150
56522
|
const AUTO_APPROVE_KEY = "autoApprove";
|
|
55151
56523
|
/** The tool permission groups Bob documents for `approval.allowed_permissions`. */
|
|
@@ -55166,7 +56538,7 @@ const BOB_AUTO_APPROVE_KEYS = /* @__PURE__ */ new Set(["skills"]);
|
|
|
55166
56538
|
const OUTSIDE_WORKSPACE_GROUP_ID = "read";
|
|
55167
56539
|
/** The only executor tool Bob's `allowedExecutors` list documents. */
|
|
55168
56540
|
const EXECUTE_COMMAND_TOOL_ID = "execute_command";
|
|
55169
|
-
const SURFACE_LABEL = `${APPROVAL_KEY}.allowedExecutors[${EXECUTE_COMMAND_TOOL_ID}].approvedCommands/deniedCommands`;
|
|
56541
|
+
const SURFACE_LABEL = `${APPROVAL_KEY$1}.allowedExecutors[${EXECUTE_COMMAND_TOOL_ID}].approvedCommands/deniedCommands`;
|
|
55170
56542
|
function asStringArray$2(value) {
|
|
55171
56543
|
if (!Array.isArray(value)) return [];
|
|
55172
56544
|
return value.filter((entry) => typeof entry === "string");
|
|
@@ -55258,7 +56630,7 @@ function warnAboutUndocumentedSettings({ override, logger }) {
|
|
|
55258
56630
|
* fetched permissions file. Switches that are already on are not repeated.
|
|
55259
56631
|
*/
|
|
55260
56632
|
function warnAboutNewAutoApprovals({ settings, override, filePath, logger }) {
|
|
55261
|
-
const existingApproval = isPlainObject$1(settings[APPROVAL_KEY]) ? settings[APPROVAL_KEY] : {};
|
|
56633
|
+
const existingApproval = isPlainObject$1(settings[APPROVAL_KEY$1]) ? settings[APPROVAL_KEY$1] : {};
|
|
55262
56634
|
const existingGroups = new Set(asStringArray$2(existingApproval.allowed_permissions));
|
|
55263
56635
|
const existingOutside = new Set((Array.isArray(existingApproval.permissionOptions) ? existingApproval.permissionOptions : []).filter(isPlainObject$1).filter((option) => option.enableOutsideWorkspace === true).map((option) => option.groupId));
|
|
55264
56636
|
const existingAutoApprove = isPlainObject$1(settings[AUTO_APPROVE_KEY]) ? settings[AUTO_APPROVE_KEY] : {};
|
|
@@ -55324,7 +56696,7 @@ function buildBobCommandLists({ permission, logger }) {
|
|
|
55324
56696
|
*/
|
|
55325
56697
|
function readBobOverride(settings) {
|
|
55326
56698
|
const override = {};
|
|
55327
|
-
const approval = settings[APPROVAL_KEY];
|
|
56699
|
+
const approval = settings[APPROVAL_KEY$1];
|
|
55328
56700
|
if (isPlainObject$1(approval)) {
|
|
55329
56701
|
const overrideApproval = {};
|
|
55330
56702
|
const groups = approval.allowed_permissions;
|
|
@@ -55453,8 +56825,8 @@ var BobPermissions = class BobPermissions extends ToolPermissions {
|
|
|
55453
56825
|
deniedCommands: denied
|
|
55454
56826
|
};
|
|
55455
56827
|
}
|
|
55456
|
-
patch[APPROVAL_KEY] = buildApproval({
|
|
55457
|
-
existingApproval: settings[APPROVAL_KEY],
|
|
56828
|
+
patch[APPROVAL_KEY$1] = buildApproval({
|
|
56829
|
+
existingApproval: settings[APPROVAL_KEY$1],
|
|
55458
56830
|
overrideApproval,
|
|
55459
56831
|
commandLists
|
|
55460
56832
|
});
|
|
@@ -55483,7 +56855,7 @@ var BobPermissions = class BobPermissions extends ToolPermissions {
|
|
|
55483
56855
|
fileContent: this.getFileContent(),
|
|
55484
56856
|
filePath: (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())
|
|
55485
56857
|
});
|
|
55486
|
-
const approval = settings[APPROVAL_KEY];
|
|
56858
|
+
const approval = settings[APPROVAL_KEY$1];
|
|
55487
56859
|
const entry = (isPlainObject$1(approval) && Array.isArray(approval.allowedExecutors) ? approval.allowedExecutors : []).find(isExecuteCommandEntry);
|
|
55488
56860
|
const rules = {};
|
|
55489
56861
|
for (const pattern of asStringArray$2(entry?.approvedCommands)) rules[pattern] = "allow";
|
|
@@ -57199,7 +58571,7 @@ const CODEBUDDY_RULE_LISTS = [
|
|
|
57199
58571
|
"ask",
|
|
57200
58572
|
"deny"
|
|
57201
58573
|
];
|
|
57202
|
-
const CATCH_ALL_PATTERN$
|
|
58574
|
+
const CATCH_ALL_PATTERN$13 = "*";
|
|
57203
58575
|
const MCP_CATEGORY = "mcp";
|
|
57204
58576
|
const MCP_RULE_PREFIX = "mcp__";
|
|
57205
58577
|
const ALL_MCP_RULE = "mcp__*";
|
|
@@ -57226,7 +58598,7 @@ const CATEGORY_TO_CODEBUDDY_TOOL = {
|
|
|
57226
58598
|
skill: "Skill"
|
|
57227
58599
|
};
|
|
57228
58600
|
const CODEBUDDY_TOOL_TO_CATEGORY = Object.fromEntries(Object.entries(CATEGORY_TO_CODEBUDDY_TOOL).map(([category, tool]) => [tool, category]));
|
|
57229
|
-
function toStringList$
|
|
58601
|
+
function toStringList$3(value) {
|
|
57230
58602
|
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
57231
58603
|
}
|
|
57232
58604
|
/**
|
|
@@ -57272,7 +58644,7 @@ function categoryOfRule$2(rule) {
|
|
|
57272
58644
|
* and reach the `Bash` rules through `honorAllToolsOnBash`.
|
|
57273
58645
|
*/
|
|
57274
58646
|
function buildCodebuddyRule({ category, pattern, action, logger }) {
|
|
57275
|
-
const catchAll = pattern === CATCH_ALL_PATTERN$
|
|
58647
|
+
const catchAll = pattern === CATCH_ALL_PATTERN$13;
|
|
57276
58648
|
if (category === MCP_CATEGORY) {
|
|
57277
58649
|
if (catchAll && action === "allow") {
|
|
57278
58650
|
logger?.warn(`CodeBuddy permissions: skipping the 'mcp' allow rule '*' — CodeBuddy honors the all-MCP wildcard 'mcp__*' in deny and ask only. Allow servers by name instead (for example 'mcp__github').`);
|
|
@@ -57281,7 +58653,7 @@ function buildCodebuddyRule({ category, pattern, action, logger }) {
|
|
|
57281
58653
|
return catchAll ? ALL_MCP_RULE : `${MCP_RULE_PREFIX}${pattern}`;
|
|
57282
58654
|
}
|
|
57283
58655
|
if (category === "*") {
|
|
57284
|
-
if (catchAll) return CATCH_ALL_PATTERN$
|
|
58656
|
+
if (catchAll) return CATCH_ALL_PATTERN$13;
|
|
57285
58657
|
logger?.warn(`CodeBuddy permissions: skipping the all-tools '*' rule '${pattern}' — CodeBuddy's standalone '*' rule takes no specifier. Write it under the category it is meant for (for example 'bash' or 'read').`);
|
|
57286
58658
|
return null;
|
|
57287
58659
|
}
|
|
@@ -57299,7 +58671,7 @@ function parseCodebuddyRule(rule) {
|
|
|
57299
58671
|
const { specifier } = splitCodebuddyRule(rule);
|
|
57300
58672
|
if (specifier === void 0) return {
|
|
57301
58673
|
category,
|
|
57302
|
-
pattern: CATCH_ALL_PATTERN$
|
|
58674
|
+
pattern: CATCH_ALL_PATTERN$13
|
|
57303
58675
|
};
|
|
57304
58676
|
const trimmed = specifier.trim();
|
|
57305
58677
|
return trimmed === "" ? null : {
|
|
@@ -57340,7 +58712,7 @@ function buildCodebuddyRuleLists({ config, existingPermissions, logger }) {
|
|
|
57340
58712
|
ask: [],
|
|
57341
58713
|
deny: []
|
|
57342
58714
|
};
|
|
57343
|
-
for (const list of CODEBUDDY_RULE_LISTS) lists[list] = toStringList$
|
|
58715
|
+
for (const list of CODEBUDDY_RULE_LISTS) lists[list] = toStringList$3(existingPermissions[list]).filter((rule) => {
|
|
57344
58716
|
const category = categoryOfRule$2(rule);
|
|
57345
58717
|
return category === void 0 || !managedCategories.has(category);
|
|
57346
58718
|
});
|
|
@@ -57431,7 +58803,7 @@ var CodebuddyPermissions = class CodebuddyPermissions extends ToolPermissions {
|
|
|
57431
58803
|
});
|
|
57432
58804
|
const permissions = isRecord$1(settings[CODEBUDDY_PERMISSIONS_KEY]) ? settings[CODEBUDDY_PERMISSIONS_KEY] : {};
|
|
57433
58805
|
const permission = {};
|
|
57434
|
-
for (const action of CODEBUDDY_RULE_LISTS) for (const rule of toStringList$
|
|
58806
|
+
for (const action of CODEBUDDY_RULE_LISTS) for (const rule of toStringList$3(permissions[action])) {
|
|
57435
58807
|
const parsed = parseCodebuddyRule(rule);
|
|
57436
58808
|
if (parsed === null || isPrototypePollutionKey(parsed.category) || isPrototypePollutionKey(parsed.pattern)) continue;
|
|
57437
58809
|
const rules = permission[parsed.category] ??= {};
|
|
@@ -59550,7 +60922,7 @@ function mapBashActionToDecision(action) {
|
|
|
59550
60922
|
//#region src/features/permissions/commandcode-permissions.ts
|
|
59551
60923
|
/** Top-level key of `.commandcode/settings.json` that rulesync owns. */
|
|
59552
60924
|
const COMMANDCODE_PERMISSIONS_KEY = "permissions";
|
|
59553
|
-
const CATCH_ALL_PATTERN$
|
|
60925
|
+
const CATCH_ALL_PATTERN$12 = "*";
|
|
59554
60926
|
const MCP_CANONICAL_PREFIX$6 = "mcp__";
|
|
59555
60927
|
const COMMANDCODE_ALL_MCP_RULE = "mcp__*";
|
|
59556
60928
|
const CATEGORY_TO_COMMANDCODE_TOOL = {
|
|
@@ -59575,14 +60947,14 @@ const COMMANDCODE_TOOL_TO_CATEGORY = {
|
|
|
59575
60947
|
web_fetch: "webfetch",
|
|
59576
60948
|
web_search: "websearch"
|
|
59577
60949
|
};
|
|
59578
|
-
const ACTION_RANK$
|
|
60950
|
+
const ACTION_RANK$6 = {
|
|
59579
60951
|
allow: 0,
|
|
59580
60952
|
ask: 1,
|
|
59581
60953
|
deny: 2
|
|
59582
60954
|
};
|
|
59583
60955
|
/** Whether `action` outranks `existing` (deny > ask > allow). */
|
|
59584
60956
|
function isStricterAction({ action, existing }) {
|
|
59585
|
-
return existing === void 0 || ACTION_RANK$
|
|
60957
|
+
return existing === void 0 || ACTION_RANK$6[action] > ACTION_RANK$6[existing];
|
|
59586
60958
|
}
|
|
59587
60959
|
/**
|
|
59588
60960
|
* Why a padded `Shell( * )` cannot stand in for the bare `Shell` in `allow`:
|
|
@@ -59605,10 +60977,10 @@ const PADDED_SHELL_WILDCARD_REASON = "a pattern narrower than the bare 'Shell' (
|
|
|
59605
60977
|
* restrictions and reach the `Shell` entries through `honorAllToolsOnBash`.
|
|
59606
60978
|
*/
|
|
59607
60979
|
function buildCommandcodeRule(category, pattern) {
|
|
59608
|
-
const catchAll = pattern === CATCH_ALL_PATTERN$
|
|
60980
|
+
const catchAll = pattern === CATCH_ALL_PATTERN$12 || pattern === "";
|
|
59609
60981
|
if (category.startsWith(MCP_CANONICAL_PREFIX$6)) return catchAll ? category : `${category}(${pattern})`;
|
|
59610
60982
|
if (category === "mcp") return catchAll ? COMMANDCODE_ALL_MCP_RULE : `${MCP_CANONICAL_PREFIX$6}${pattern}`;
|
|
59611
|
-
if (category === "*") return catchAll ? CATCH_ALL_PATTERN$
|
|
60983
|
+
if (category === "*") return catchAll ? CATCH_ALL_PATTERN$12 : null;
|
|
59612
60984
|
const tool = Object.hasOwn(CATEGORY_TO_COMMANDCODE_TOOL, category) ? CATEGORY_TO_COMMANDCODE_TOOL[category] : void 0;
|
|
59613
60985
|
if (tool === void 0) return null;
|
|
59614
60986
|
return catchAll ? tool : `${tool}(${pattern})`;
|
|
@@ -59644,7 +61016,7 @@ function splitCommandcodeRule(rule) {
|
|
|
59644
61016
|
}
|
|
59645
61017
|
/** Whether a specifier means "no specifier" to Command Code: only `` and `*` do. */
|
|
59646
61018
|
function isCatchAllSpecifier(inner) {
|
|
59647
|
-
return inner === "" || inner === CATCH_ALL_PATTERN$
|
|
61019
|
+
return inner === "" || inner === CATCH_ALL_PATTERN$12;
|
|
59648
61020
|
}
|
|
59649
61021
|
/**
|
|
59650
61022
|
* Read an MCP-shaped rule the way Command Code does (`command-code` 1.54.0,
|
|
@@ -59678,34 +61050,34 @@ function parseMcpCommandcodeRule({ tool, inner }) {
|
|
|
59678
61050
|
if (server.length === 0) return null;
|
|
59679
61051
|
const everyServer = {
|
|
59680
61052
|
category: "mcp",
|
|
59681
|
-
pattern: CATCH_ALL_PATTERN$
|
|
61053
|
+
pattern: CATCH_ALL_PATTERN$12,
|
|
59682
61054
|
notInAllow: true
|
|
59683
61055
|
};
|
|
59684
61056
|
if (tool.startsWith(MCP_CANONICAL_PREFIX$6)) {
|
|
59685
|
-
if (server === CATCH_ALL_PATTERN$
|
|
59686
|
-
if (server.includes(CATCH_ALL_PATTERN$
|
|
61057
|
+
if (server === CATCH_ALL_PATTERN$12) return everyServer;
|
|
61058
|
+
if (server.includes(CATCH_ALL_PATTERN$12)) return null;
|
|
59687
61059
|
return {
|
|
59688
|
-
category: rest === "" || rest === CATCH_ALL_PATTERN$
|
|
59689
|
-
pattern: CATCH_ALL_PATTERN$
|
|
61060
|
+
category: rest === "" || rest === CATCH_ALL_PATTERN$12 ? `${MCP_CANONICAL_PREFIX$6}${server}` : `${MCP_CANONICAL_PREFIX$6}${remainder}`,
|
|
61061
|
+
pattern: CATCH_ALL_PATTERN$12,
|
|
59690
61062
|
notInAllow: false
|
|
59691
61063
|
};
|
|
59692
61064
|
}
|
|
59693
61065
|
const category = `${MCP_CANONICAL_PREFIX$6}${remainder}`;
|
|
59694
|
-
const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$
|
|
61066
|
+
const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$12);
|
|
59695
61067
|
if (!isCatchAllSpecifier(inner)) return namesTool ? {
|
|
59696
61068
|
category,
|
|
59697
61069
|
pattern: inner,
|
|
59698
61070
|
notInAllow: true
|
|
59699
61071
|
} : null;
|
|
59700
|
-
if (remainder === CATCH_ALL_PATTERN$
|
|
59701
|
-
if (rest === CATCH_ALL_PATTERN$
|
|
61072
|
+
if (remainder === CATCH_ALL_PATTERN$12) return everyServer;
|
|
61073
|
+
if (rest === CATCH_ALL_PATTERN$12 && !server.includes(CATCH_ALL_PATTERN$12)) return {
|
|
59702
61074
|
category: `${MCP_CANONICAL_PREFIX$6}${server}`,
|
|
59703
|
-
pattern: CATCH_ALL_PATTERN$
|
|
61075
|
+
pattern: CATCH_ALL_PATTERN$12,
|
|
59704
61076
|
notInAllow: true
|
|
59705
61077
|
};
|
|
59706
61078
|
return namesTool ? {
|
|
59707
61079
|
category,
|
|
59708
|
-
pattern: CATCH_ALL_PATTERN$
|
|
61080
|
+
pattern: CATCH_ALL_PATTERN$12,
|
|
59709
61081
|
notInAllow: false
|
|
59710
61082
|
} : null;
|
|
59711
61083
|
}
|
|
@@ -59729,14 +61101,14 @@ function friendlyToolRule({ category, inner }) {
|
|
|
59729
61101
|
if (category === "bash") {
|
|
59730
61102
|
if (isCatchAllSpecifier(inner)) return {
|
|
59731
61103
|
category,
|
|
59732
|
-
pattern: CATCH_ALL_PATTERN$
|
|
61104
|
+
pattern: CATCH_ALL_PATTERN$12,
|
|
59733
61105
|
notInAllow: false
|
|
59734
61106
|
};
|
|
59735
61107
|
const normalized = inner.trim().replace(/\s+/g, " ");
|
|
59736
61108
|
if (normalized === "" || normalized === ":*") return null;
|
|
59737
61109
|
return isCatchAllSpecifier(normalized) ? {
|
|
59738
61110
|
category,
|
|
59739
|
-
pattern: CATCH_ALL_PATTERN$
|
|
61111
|
+
pattern: CATCH_ALL_PATTERN$12,
|
|
59740
61112
|
notInAllow: true
|
|
59741
61113
|
} : {
|
|
59742
61114
|
category,
|
|
@@ -59746,7 +61118,7 @@ function friendlyToolRule({ category, inner }) {
|
|
|
59746
61118
|
}
|
|
59747
61119
|
if (isCatchAllSpecifier(inner)) return {
|
|
59748
61120
|
category,
|
|
59749
|
-
pattern: CATCH_ALL_PATTERN$
|
|
61121
|
+
pattern: CATCH_ALL_PATTERN$12,
|
|
59750
61122
|
notInAllow: false
|
|
59751
61123
|
};
|
|
59752
61124
|
return isCatchAllSpecifier(inner.trim()) ? null : {
|
|
@@ -59765,9 +61137,9 @@ function friendlyToolRule({ category, inner }) {
|
|
|
59765
61137
|
*/
|
|
59766
61138
|
function parseCommandcodeRule(rule) {
|
|
59767
61139
|
const { tool, inner } = splitCommandcodeRule(rule);
|
|
59768
|
-
if (tool === CATCH_ALL_PATTERN$
|
|
61140
|
+
if (tool === CATCH_ALL_PATTERN$12) return isCatchAllSpecifier(inner) ? {
|
|
59769
61141
|
category: "*",
|
|
59770
|
-
pattern: CATCH_ALL_PATTERN$
|
|
61142
|
+
pattern: CATCH_ALL_PATTERN$12,
|
|
59771
61143
|
notInAllow: true
|
|
59772
61144
|
} : null;
|
|
59773
61145
|
const lowered = tool.toLowerCase();
|
|
@@ -59837,7 +61209,7 @@ function preservedRules({ existingPermissions, key, managedCategories, written,
|
|
|
59837
61209
|
}).filter((rule) => {
|
|
59838
61210
|
const parsed = parseCommandcodeRule(rule);
|
|
59839
61211
|
if (parsed === null || !managedCategories.has(parsed.category)) return true;
|
|
59840
|
-
const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$
|
|
61212
|
+
const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$12})`);
|
|
59841
61213
|
if (key !== "allow" && isStricterAction({
|
|
59842
61214
|
action: key,
|
|
59843
61215
|
existing: replacement
|
|
@@ -59894,7 +61266,7 @@ function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
|
|
|
59894
61266
|
const ranked = /* @__PURE__ */ new Map();
|
|
59895
61267
|
const managedCategories = /* @__PURE__ */ new Set();
|
|
59896
61268
|
for (const [category, rules] of Object.entries(permission)) {
|
|
59897
|
-
const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$
|
|
61269
|
+
const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$12);
|
|
59898
61270
|
const managedCategory = categoryRule === null ? null : parseCommandcodeRule(categoryRule);
|
|
59899
61271
|
if (managedCategory !== null) managedCategories.add(managedCategory.category);
|
|
59900
61272
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
@@ -60165,7 +61537,7 @@ function parseCommandcodeSettings({ fileContent, relativePath }) {
|
|
|
60165
61537
|
//#endregion
|
|
60166
61538
|
//#region src/features/permissions/continue-permissions.ts
|
|
60167
61539
|
const CONTINUE_GLOBAL_ONLY_MESSAGE = "Continue permissions are global-only; use --global to sync ~/.continue/permissions.yaml";
|
|
60168
|
-
const CATCH_ALL_PATTERN$
|
|
61540
|
+
const CATCH_ALL_PATTERN$11 = "*";
|
|
60169
61541
|
/**
|
|
60170
61542
|
* A run of `*` (`**`, `***`) is the catch-all spelled another way: Continue
|
|
60171
61543
|
* would match `Tool(**)` against every argument just as the bare tool name
|
|
@@ -60173,7 +61545,7 @@ const CATCH_ALL_PATTERN$10 = "*";
|
|
|
60173
61545
|
* the bare entry instead of being skipped as a pattern-specific rule.
|
|
60174
61546
|
*/
|
|
60175
61547
|
function normalizeCatchAllPattern$1(pattern) {
|
|
60176
|
-
return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$
|
|
61548
|
+
return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$11 : pattern;
|
|
60177
61549
|
}
|
|
60178
61550
|
const CANONICAL_TO_CONTINUE_TOOL_NAMES = {
|
|
60179
61551
|
bash: "Bash",
|
|
@@ -60210,7 +61582,7 @@ function toCanonicalToolName$6(continueName) {
|
|
|
60210
61582
|
* `Tool(pattern)`.
|
|
60211
61583
|
*/
|
|
60212
61584
|
function buildContinuePermissionEntry(toolName, pattern) {
|
|
60213
|
-
return pattern === CATCH_ALL_PATTERN$
|
|
61585
|
+
return pattern === CATCH_ALL_PATTERN$11 ? toolName : `${toolName}(${pattern})`;
|
|
60214
61586
|
}
|
|
60215
61587
|
/**
|
|
60216
61588
|
* Split a permissions.yaml entry into its tool name and argument pattern, or
|
|
@@ -60224,7 +61596,7 @@ function parseContinuePermissionEntry(entry) {
|
|
|
60224
61596
|
if (toolName === "") return;
|
|
60225
61597
|
return {
|
|
60226
61598
|
toolName,
|
|
60227
|
-
pattern: pattern === "" ? CATCH_ALL_PATTERN$
|
|
61599
|
+
pattern: pattern === "" ? CATCH_ALL_PATTERN$11 : pattern
|
|
60228
61600
|
};
|
|
60229
61601
|
}
|
|
60230
61602
|
/**
|
|
@@ -60398,7 +61770,7 @@ function convertRulesyncToContinuePermissions({ config, logger }) {
|
|
|
60398
61770
|
logger?.warn(`Continue permissions.yaml cannot hold a parenthesis inside a pattern, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
|
|
60399
61771
|
continue;
|
|
60400
61772
|
}
|
|
60401
|
-
if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$
|
|
61773
|
+
if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$11) {
|
|
60402
61774
|
logger?.warn(`Continue permissions.yaml cannot scope a pattern to every tool, so the "${action}" rule for "*" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
|
|
60403
61775
|
continue;
|
|
60404
61776
|
}
|
|
@@ -60830,7 +62202,7 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
|
|
|
60830
62202
|
* arrays are accepted only in MDM/enterprise managed settings, and session
|
|
60831
62203
|
* tool approvals are machine-written to `permissions-config.json`.
|
|
60832
62204
|
*/
|
|
60833
|
-
const WEBFETCH_CATEGORY = "webfetch";
|
|
62205
|
+
const WEBFETCH_CATEGORY$1 = "webfetch";
|
|
60834
62206
|
/** `~/.copilot/settings.json` and `.github/copilot/settings.json` keys. */
|
|
60835
62207
|
const ALLOWED_URLS_KEY = "allowedUrls";
|
|
60836
62208
|
const DENIED_URLS_KEY = "deniedUrls";
|
|
@@ -60917,7 +62289,7 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
|
|
|
60917
62289
|
const paths = CopilotcliPermissions.getSettablePaths({ global });
|
|
60918
62290
|
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
60919
62291
|
const existingContent = await readFileContentOrNull(filePath) ?? "{}";
|
|
60920
|
-
const rules = rulesyncPermissions.getJson().permission[WEBFETCH_CATEGORY];
|
|
62292
|
+
const rules = rulesyncPermissions.getJson().permission[WEBFETCH_CATEGORY$1];
|
|
60921
62293
|
const patch = {};
|
|
60922
62294
|
if (rules !== void 0) {
|
|
60923
62295
|
const { allowedUrls, deniedUrls } = splitUrlRules(rules);
|
|
@@ -60945,7 +62317,7 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
|
|
|
60945
62317
|
const rules = {};
|
|
60946
62318
|
for (const pattern of toUrlList(settings[DENIED_URLS_KEY])) rules[pattern] = "deny";
|
|
60947
62319
|
if (this.global) for (const pattern of toUrlList(settings[ALLOWED_URLS_KEY])) rules[pattern] ??= "allow";
|
|
60948
|
-
const permission = Object.keys(rules).length > 0 ? { [WEBFETCH_CATEGORY]: rules } : {};
|
|
62320
|
+
const permission = Object.keys(rules).length > 0 ? { [WEBFETCH_CATEGORY$1]: rules } : {};
|
|
60949
62321
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
|
|
60950
62322
|
}
|
|
60951
62323
|
parseSettings() {
|
|
@@ -60980,14 +62352,14 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
|
|
|
60980
62352
|
};
|
|
60981
62353
|
//#endregion
|
|
60982
62354
|
//#region src/features/permissions/crush-permissions.ts
|
|
60983
|
-
const CATCH_ALL_PATTERN$
|
|
62355
|
+
const CATCH_ALL_PATTERN$10 = "*";
|
|
60984
62356
|
/**
|
|
60985
62357
|
* A run of `*` (`**`, `***`) is the catch-all spelled another way: Crush has no
|
|
60986
62358
|
* argument patterns at all, so it is the only pattern a rule can carry and
|
|
60987
62359
|
* still be written.
|
|
60988
62360
|
*/
|
|
60989
62361
|
function normalizeCatchAllPattern(pattern) {
|
|
60990
|
-
return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$
|
|
62362
|
+
return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$10 : pattern;
|
|
60991
62363
|
}
|
|
60992
62364
|
const CANONICAL_TO_CRUSH_TOOL_NAMES = {
|
|
60993
62365
|
bash: "bash",
|
|
@@ -61186,7 +62558,7 @@ function resolveCategoryAction({ category, toolName, rules, logger }) {
|
|
|
61186
62558
|
for (const [rawPattern, action] of Object.entries(rules)) {
|
|
61187
62559
|
if (isPrototypePollutionKey(rawPattern)) continue;
|
|
61188
62560
|
const pattern = normalizeCatchAllPattern(rawPattern);
|
|
61189
|
-
if (pattern === CATCH_ALL_PATTERN$
|
|
62561
|
+
if (pattern === CATCH_ALL_PATTERN$10) {
|
|
61190
62562
|
catchAllAction = action;
|
|
61191
62563
|
continue;
|
|
61192
62564
|
}
|
|
@@ -61259,11 +62631,11 @@ function convertCrushListsToRulesync({ allowed, disabled }) {
|
|
|
61259
62631
|
};
|
|
61260
62632
|
for (const entry of disabled) {
|
|
61261
62633
|
if (entry === "" || isPrototypePollutionKey(entry)) continue;
|
|
61262
|
-
bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$
|
|
62634
|
+
bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$10] = "deny";
|
|
61263
62635
|
}
|
|
61264
62636
|
for (const entry of allowed) {
|
|
61265
62637
|
if (entry === "" || entry.includes(":") || isPrototypePollutionKey(entry)) continue;
|
|
61266
|
-
bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$
|
|
62638
|
+
bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$10] ??= "allow";
|
|
61267
62639
|
}
|
|
61268
62640
|
return { permission };
|
|
61269
62641
|
}
|
|
@@ -62705,7 +64077,8 @@ const DEVIN_RESTRICTION_LOSING_SANDBOX_PATHS = [
|
|
|
62705
64077
|
* (MCP moved to the dedicated mcp_config.json in v3000.3), so reads and writes
|
|
62706
64078
|
* merge into the existing JSON and the file is never deleted; only the keys
|
|
62707
64079
|
* this feature manages are rewritten — `permissions`, plus `sandbox` in global
|
|
62708
|
-
* mode
|
|
64080
|
+
* mode and the plugin governance lists (`requiredPlugins` / `optionalPlugins` /
|
|
64081
|
+
* `forbiddenPlugins`) in project mode when the `devin` override authors them.
|
|
62709
64082
|
*
|
|
62710
64083
|
* The sibling `sandbox` block — which decides what a permitted command may
|
|
62711
64084
|
* reach rather than which commands are permitted — has no canonical category
|
|
@@ -62726,7 +64099,8 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
62726
64099
|
/**
|
|
62727
64100
|
* config.json may carry the MCP/hooks features' keys, so it is never deleted;
|
|
62728
64101
|
* only the keys this feature manages are rewritten — `permissions`, plus
|
|
62729
|
-
* `sandbox`
|
|
64102
|
+
* `sandbox` (global) and the plugin governance lists (project) when the
|
|
64103
|
+
* `devin` override authors them.
|
|
62730
64104
|
*/
|
|
62731
64105
|
isDeletable() {
|
|
62732
64106
|
return false;
|
|
@@ -62821,6 +64195,13 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
62821
64195
|
});
|
|
62822
64196
|
} else if (Object.keys(authoredSandboxRecord).length > 0) logger?.warn("Devin reads 'sandbox' from the user config only, so the 'devin.sandbox' override was dropped from the project config. Generate with --global to author it.");
|
|
62823
64197
|
}
|
|
64198
|
+
applyPluginGovernanceOverride({
|
|
64199
|
+
override: config.devin,
|
|
64200
|
+
settings,
|
|
64201
|
+
patch,
|
|
64202
|
+
global,
|
|
64203
|
+
logger
|
|
64204
|
+
});
|
|
62824
64205
|
return new DevinPermissions({
|
|
62825
64206
|
outputRoot,
|
|
62826
64207
|
relativeDirPath: paths.relativeDirPath,
|
|
@@ -62835,7 +64216,7 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
62835
64216
|
validate
|
|
62836
64217
|
});
|
|
62837
64218
|
}
|
|
62838
|
-
toRulesyncPermissions() {
|
|
64219
|
+
toRulesyncPermissions({ logger } = {}) {
|
|
62839
64220
|
let settings;
|
|
62840
64221
|
try {
|
|
62841
64222
|
const parsed = JSON.parse(this.getFileContent());
|
|
@@ -62850,8 +64231,16 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
62850
64231
|
deny: Array.isArray(permissions.deny) ? permissions.deny : []
|
|
62851
64232
|
});
|
|
62852
64233
|
const sandbox = asDevinRecord(settings.sandbox);
|
|
64234
|
+
const devinOverride = {};
|
|
64235
|
+
if (Object.keys(sandbox).length > 0) devinOverride.sandbox = sandbox;
|
|
64236
|
+
for (const key of DEVIN_PLUGIN_GOVERNANCE_KEYS) {
|
|
64237
|
+
const value = settings[key];
|
|
64238
|
+
if (value === void 0) continue;
|
|
64239
|
+
if (DEVIN_PLUGIN_GOVERNANCE_SCHEMAS[key].safeParse(value).success) devinOverride[key] = value;
|
|
64240
|
+
else logger?.warn(`Skipped Devin \`${key}\` on import: ${key === "forbiddenPlugins" ? "expected an array of strings" : "expected an array of plugin sources (strings or objects)"}.`);
|
|
64241
|
+
}
|
|
62853
64242
|
const result = { ...config };
|
|
62854
|
-
if (Object.keys(
|
|
64243
|
+
if (Object.keys(devinOverride).length > 0) result.devin = devinOverride;
|
|
62855
64244
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
|
|
62856
64245
|
}
|
|
62857
64246
|
validate() {
|
|
@@ -62871,6 +64260,42 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
62871
64260
|
}
|
|
62872
64261
|
};
|
|
62873
64262
|
/**
|
|
64263
|
+
* The repo-level plugin governance lists of a checkout's `.devin/config.json`.
|
|
64264
|
+
*
|
|
64265
|
+
* @see https://docs.devin.ai/cli/extensibility/plugins/overview
|
|
64266
|
+
*/
|
|
64267
|
+
const DEVIN_PLUGIN_GOVERNANCE_KEYS = [
|
|
64268
|
+
"requiredPlugins",
|
|
64269
|
+
"optionalPlugins",
|
|
64270
|
+
"forbiddenPlugins"
|
|
64271
|
+
];
|
|
64272
|
+
/**
|
|
64273
|
+
* Patch the plugin governance lists the `devin` override authors into the
|
|
64274
|
+
* project `.devin/config.json`. Each authored list replaces the file's list
|
|
64275
|
+
* whole; a list it does not author is left as the file holds it. Devin reads these
|
|
64276
|
+
* lists only from a checkout's `.devin/config.json`, so in global mode they are
|
|
64277
|
+
* dropped with a warning. Losing a `forbiddenPlugins` entry the file held is
|
|
64278
|
+
* announced, since that lifts a restriction. A non-empty `requiredPlugins` that changes the file
|
|
64279
|
+
* is announced, since Devin installs required plugins automatically and a
|
|
64280
|
+
* plugin can ship its own hooks and MCP servers.
|
|
64281
|
+
*/
|
|
64282
|
+
function applyPluginGovernanceOverride({ override, settings, patch, global, logger }) {
|
|
64283
|
+
const authoredKeys = DEVIN_PLUGIN_GOVERNANCE_KEYS.filter((key) => override?.[key] !== void 0);
|
|
64284
|
+
if (authoredKeys.length === 0) return;
|
|
64285
|
+
if (global) {
|
|
64286
|
+
logger?.warn(`Devin reads the plugin governance lists (${authoredKeys.join(", ")}) from a checkout's '.devin/config.json' only, so they were dropped from the user config. Generate without --global to author them.`);
|
|
64287
|
+
return;
|
|
64288
|
+
}
|
|
64289
|
+
for (const key of authoredKeys) patch[key] = override?.[key];
|
|
64290
|
+
const forbidden = override?.forbiddenPlugins;
|
|
64291
|
+
if (Array.isArray(forbidden) && Array.isArray(settings.forbiddenPlugins)) {
|
|
64292
|
+
const lost = settings.forbiddenPlugins.filter((entry) => !forbidden.includes(entry));
|
|
64293
|
+
if (lost.length > 0) logger?.warn(`Replacing Devin \`forbiddenPlugins\` drops ${lost.map(quoteValueForWarning).join(", ")} from '.devin/config.json': those plugins are no longer forbidden at the repo level.`);
|
|
64294
|
+
}
|
|
64295
|
+
const required = override?.requiredPlugins;
|
|
64296
|
+
if (Array.isArray(required) && required.length > 0 && !(0, node_util.isDeepStrictEqual)(required, settings.requiredPlugins)) logger?.warn(`Writing Devin \`requiredPlugins\` (${required.map(quoteValueForWarning).join(", ")}): Devin installs required plugins automatically, and a plugin can ship its own hooks and MCP servers.`);
|
|
64297
|
+
}
|
|
64298
|
+
/**
|
|
62874
64299
|
* Collapse the pattern rules of a bare-only scope to the single action Devin
|
|
62875
64300
|
* can hold for it, using deny > ask > allow precedence. A map without a
|
|
62876
64301
|
* catch-all grants nothing to unmatched inputs, so an implicit `ask` joins the
|
|
@@ -63128,7 +64553,7 @@ function convertFactorydroidToRulesyncPermissions({ allow, deny }) {
|
|
|
63128
64553
|
//#region src/features/permissions/goose-permissions.ts
|
|
63129
64554
|
const GOOSE_GLOBAL_ONLY_MESSAGE = "Goose permissions are global-only; use --global to sync ~/.config/goose/permission.yaml";
|
|
63130
64555
|
const GOOSE_USER_KEY = "user";
|
|
63131
|
-
const CATCH_ALL_PATTERN$
|
|
64556
|
+
const CATCH_ALL_PATTERN$9 = "*";
|
|
63132
64557
|
const RULESYNC_TO_GOOSE_TOOL_NAME = { bash: "shell" };
|
|
63133
64558
|
const GOOSE_TO_RULESYNC_TOOL_NAMES = {
|
|
63134
64559
|
shell: ["bash"],
|
|
@@ -63140,7 +64565,7 @@ const ACTION_TO_GOOSE_LIST = {
|
|
|
63140
64565
|
ask: "ask_before",
|
|
63141
64566
|
deny: "never_allow"
|
|
63142
64567
|
};
|
|
63143
|
-
const ACTION_RANK$
|
|
64568
|
+
const ACTION_RANK$5 = {
|
|
63144
64569
|
allow: 0,
|
|
63145
64570
|
ask: 1,
|
|
63146
64571
|
deny: 2
|
|
@@ -63292,14 +64717,14 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
|
|
|
63292
64717
|
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
63293
64718
|
const toolName = RULESYNC_TO_GOOSE_TOOL_NAME[category] ?? category;
|
|
63294
64719
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
63295
|
-
if (pattern !== CATCH_ALL_PATTERN$
|
|
64720
|
+
if (pattern !== CATCH_ALL_PATTERN$9) {
|
|
63296
64721
|
logger?.warn(`Goose permission.yaml lists whole tool names, so the per-pattern "${action}" rule for "${category}" (pattern "${pattern}") cannot be expressed and was skipped.`);
|
|
63297
64722
|
continue;
|
|
63298
64723
|
}
|
|
63299
64724
|
const previous = assigned.get(toolName);
|
|
63300
64725
|
if (previous === action) continue;
|
|
63301
64726
|
if (previous !== void 0) {
|
|
63302
|
-
const stricter = ACTION_RANK$
|
|
64727
|
+
const stricter = ACTION_RANK$5[action] > ACTION_RANK$5[previous] ? action : previous;
|
|
63303
64728
|
logger?.warn(`Goose maps "${category}" onto the "${toolName}" tool, which already has a conflicting permission ("${previous}"). Keeping the stricter "${stricter}" (deny > ask > allow).`);
|
|
63304
64729
|
if (stricter === previous) continue;
|
|
63305
64730
|
const previousList = lists[ACTION_TO_GOOSE_LIST[previous]];
|
|
@@ -63323,7 +64748,7 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
|
|
|
63323
64748
|
const action = GOOSE_LIST_TO_ACTION[key];
|
|
63324
64749
|
for (const toolName of toolNames) for (const category of GOOSE_TO_RULESYNC_TOOL_NAMES[toolName] ?? [toolName]) {
|
|
63325
64750
|
permission[category] ??= {};
|
|
63326
|
-
permission[category][CATCH_ALL_PATTERN$
|
|
64751
|
+
permission[category][CATCH_ALL_PATTERN$9] = action;
|
|
63327
64752
|
}
|
|
63328
64753
|
}
|
|
63329
64754
|
return { permission };
|
|
@@ -63334,7 +64759,7 @@ const GROKCLI_UI_KEY = "ui";
|
|
|
63334
64759
|
const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
|
|
63335
64760
|
const GROKCLI_PERMISSION_KEY = "permission";
|
|
63336
64761
|
const GROKCLI_AUTO_PERMISSION_MODE = "auto";
|
|
63337
|
-
const CATCH_ALL_PATTERN$
|
|
64762
|
+
const CATCH_ALL_PATTERN$8 = "*";
|
|
63338
64763
|
const MCP_CANONICAL_PREFIX$3 = "mcp__";
|
|
63339
64764
|
const CATEGORY_TO_GROK_TOOL = {
|
|
63340
64765
|
bash: "Bash",
|
|
@@ -63380,15 +64805,15 @@ const GROKCLI_PERMISSION_RULES_KEY = "rules";
|
|
|
63380
64805
|
* under it has no Grok spelling and returns `null`.
|
|
63381
64806
|
*/
|
|
63382
64807
|
function buildGrokEntry(category, pattern) {
|
|
63383
|
-
if (category === "*") return pattern === CATCH_ALL_PATTERN$
|
|
64808
|
+
if (category === "*") return pattern === CATCH_ALL_PATTERN$8 ? GROK_ALL_TOOLS_ENTRY : null;
|
|
63384
64809
|
if (category.startsWith(MCP_CANONICAL_PREFIX$3)) {
|
|
63385
64810
|
const remainder = category.slice(5);
|
|
63386
64811
|
return remainder.length > 0 ? `${GROK_MCP_TOOL}(${remainder})` : GROK_MCP_TOOL;
|
|
63387
64812
|
}
|
|
63388
|
-
if (category === "mcp") return pattern === CATCH_ALL_PATTERN$
|
|
64813
|
+
if (category === "mcp") return pattern === CATCH_ALL_PATTERN$8 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
|
|
63389
64814
|
const tool = CATEGORY_TO_GROK_TOOL[category];
|
|
63390
64815
|
if (tool === void 0) return null;
|
|
63391
|
-
return pattern === CATCH_ALL_PATTERN$
|
|
64816
|
+
return pattern === CATCH_ALL_PATTERN$8 || pattern === "" ? tool : `${tool}(${pattern})`;
|
|
63392
64817
|
}
|
|
63393
64818
|
/**
|
|
63394
64819
|
* Parse a Grok Claude-style permission entry back into a canonical category +
|
|
@@ -63410,21 +64835,21 @@ function parseGrokEntry(entry) {
|
|
|
63410
64835
|
}
|
|
63411
64836
|
if (parenIndex === -1 && tool === GROK_ALL_TOOLS_ENTRY) return {
|
|
63412
64837
|
category: "*",
|
|
63413
|
-
pattern: CATCH_ALL_PATTERN$
|
|
64838
|
+
pattern: CATCH_ALL_PATTERN$8
|
|
63414
64839
|
};
|
|
63415
64840
|
if (parenIndex === -1 && tool.startsWith(MCP_CANONICAL_PREFIX$3)) return parseClaudeStyleMcpEntry(tool);
|
|
63416
64841
|
if (tool === GROK_MCP_TOOL) return inner.length > 0 ? {
|
|
63417
64842
|
category: `${MCP_CANONICAL_PREFIX$3}${inner}`,
|
|
63418
|
-
pattern: CATCH_ALL_PATTERN$
|
|
64843
|
+
pattern: CATCH_ALL_PATTERN$8
|
|
63419
64844
|
} : {
|
|
63420
64845
|
category: "mcp",
|
|
63421
|
-
pattern: CATCH_ALL_PATTERN$
|
|
64846
|
+
pattern: CATCH_ALL_PATTERN$8
|
|
63422
64847
|
};
|
|
63423
64848
|
const category = GROK_TOOL_TO_CATEGORY[tool];
|
|
63424
64849
|
if (category === void 0) return null;
|
|
63425
64850
|
return {
|
|
63426
64851
|
category,
|
|
63427
|
-
pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$
|
|
64852
|
+
pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$8
|
|
63428
64853
|
};
|
|
63429
64854
|
}
|
|
63430
64855
|
/**
|
|
@@ -63441,14 +64866,14 @@ function parseGrokEntry(entry) {
|
|
|
63441
64866
|
function parseClaudeStyleMcpEntry(entry) {
|
|
63442
64867
|
const rest = entry.slice(5);
|
|
63443
64868
|
if (rest.length === 0) return null;
|
|
63444
|
-
if (rest === CATCH_ALL_PATTERN$
|
|
64869
|
+
if (rest === CATCH_ALL_PATTERN$8) return {
|
|
63445
64870
|
category: "mcp",
|
|
63446
|
-
pattern: CATCH_ALL_PATTERN$
|
|
64871
|
+
pattern: CATCH_ALL_PATTERN$8
|
|
63447
64872
|
};
|
|
63448
64873
|
const address = rest.includes("__") ? rest : `${rest}__*`;
|
|
63449
64874
|
return {
|
|
63450
64875
|
category: `${MCP_CANONICAL_PREFIX$3}${address}`,
|
|
63451
|
-
pattern: CATCH_ALL_PATTERN$
|
|
64876
|
+
pattern: CATCH_ALL_PATTERN$8
|
|
63452
64877
|
};
|
|
63453
64878
|
}
|
|
63454
64879
|
/**
|
|
@@ -63469,20 +64894,20 @@ function parseGrokRule(rule) {
|
|
|
63469
64894
|
if (typeof tool !== "string") return null;
|
|
63470
64895
|
if (pattern !== void 0 && typeof pattern !== "string") return null;
|
|
63471
64896
|
const trimmedPattern = pattern?.trim() ?? "";
|
|
63472
|
-
const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$
|
|
64897
|
+
const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$8;
|
|
63473
64898
|
const lowerTool = tool.trim().toLowerCase();
|
|
63474
|
-
if (lowerTool === GROK_VERBOSE_ANY_TOOL) return resolvedPattern === CATCH_ALL_PATTERN$
|
|
64899
|
+
if (lowerTool === GROK_VERBOSE_ANY_TOOL) return resolvedPattern === CATCH_ALL_PATTERN$8 ? {
|
|
63475
64900
|
category: "*",
|
|
63476
|
-
pattern: CATCH_ALL_PATTERN$
|
|
64901
|
+
pattern: CATCH_ALL_PATTERN$8,
|
|
63477
64902
|
action
|
|
63478
64903
|
} : null;
|
|
63479
|
-
if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$
|
|
64904
|
+
if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$8 ? {
|
|
63480
64905
|
category: "mcp",
|
|
63481
|
-
pattern: CATCH_ALL_PATTERN$
|
|
64906
|
+
pattern: CATCH_ALL_PATTERN$8,
|
|
63482
64907
|
action
|
|
63483
64908
|
} : {
|
|
63484
64909
|
category: `${MCP_CANONICAL_PREFIX$3}${resolvedPattern}`,
|
|
63485
|
-
pattern: CATCH_ALL_PATTERN$
|
|
64910
|
+
pattern: CATCH_ALL_PATTERN$8,
|
|
63486
64911
|
action
|
|
63487
64912
|
};
|
|
63488
64913
|
if (!Object.hasOwn(GROK_TOOL_TO_CATEGORY_LOWER, lowerTool)) return null;
|
|
@@ -63635,7 +65060,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
|
63635
65060
|
throw new Error(`Failed to parse Grok config.toml content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
63636
65061
|
}
|
|
63637
65062
|
const fineGrained = parseGrokPermissionArrays(isRecord$1(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {});
|
|
63638
|
-
const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$
|
|
65063
|
+
const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$8]: legacyModeAction(parsed) } } };
|
|
63639
65064
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
|
|
63640
65065
|
}
|
|
63641
65066
|
validate() {
|
|
@@ -63655,7 +65080,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
|
63655
65080
|
});
|
|
63656
65081
|
}
|
|
63657
65082
|
};
|
|
63658
|
-
const ACTION_RANK$
|
|
65083
|
+
const ACTION_RANK$4 = {
|
|
63659
65084
|
allow: 0,
|
|
63660
65085
|
ask: 1,
|
|
63661
65086
|
deny: 2
|
|
@@ -63690,7 +65115,7 @@ function buildGrokPermissionArrays(config, existingPermission, logger) {
|
|
|
63690
65115
|
}
|
|
63691
65116
|
const existing = ranked.get(entry);
|
|
63692
65117
|
if (existing !== void 0 && existing !== action) logger?.warn(`Grok permission entry '${entry}' received conflicting actions ('${existing}' and '${action}'); keeping the stricter one (deny > ask > allow).`);
|
|
63693
|
-
if (existing === void 0 || ACTION_RANK$
|
|
65118
|
+
if (existing === void 0 || ACTION_RANK$4[action] > ACTION_RANK$4[existing]) ranked.set(entry, action);
|
|
63694
65119
|
}
|
|
63695
65120
|
warnRemovedAllToolsRestrictions({
|
|
63696
65121
|
existingPermission,
|
|
@@ -63726,7 +65151,7 @@ function warnRemovedAllToolsRestrictions({ existingPermission, ranked, logger })
|
|
|
63726
65151
|
const entries = existingPermission[action];
|
|
63727
65152
|
if (!isStringArray$1(entries)) continue;
|
|
63728
65153
|
if (!entries.some((entry) => parseGrokEntry(entry)?.category === "*")) continue;
|
|
63729
|
-
if (generated !== void 0 && ACTION_RANK$
|
|
65154
|
+
if (generated !== void 0 && ACTION_RANK$4[generated] >= ACTION_RANK$4[action]) continue;
|
|
63730
65155
|
logger.warn(`Grok CLI permissions: removing the existing bare '*' entry from the '${action}' array — Rulesync now manages it through the all-tools '*' category, and the canonical config has no '*' '${action}' rule. Add "*": { "*": "${action}" } to keep restricting every tool.`);
|
|
63731
65156
|
}
|
|
63732
65157
|
}
|
|
@@ -63741,7 +65166,7 @@ function warnEntriesShadowedByAllTools({ ranked, logger }) {
|
|
|
63741
65166
|
if (!logger) return;
|
|
63742
65167
|
const allTools = ranked.get(GROK_ALL_TOOLS_ENTRY);
|
|
63743
65168
|
if (allTools === void 0 || allTools === "allow") return;
|
|
63744
|
-
const shadowed = [...ranked].filter(([entry, action]) => entry !== GROK_ALL_TOOLS_ENTRY && ACTION_RANK$
|
|
65169
|
+
const shadowed = [...ranked].filter(([entry, action]) => entry !== GROK_ALL_TOOLS_ENTRY && ACTION_RANK$4[action] < ACTION_RANK$4[allTools]).map(([entry]) => entry).toSorted();
|
|
63745
65170
|
if (shadowed.length === 0) return;
|
|
63746
65171
|
logger.warn(`Grok CLI permissions: the bare '*' '${allTools}' rule applies to every tool, and Grok evaluates deny > ask > allow, so these less strict entries never take effect: ${shadowed.join(", ")}.`);
|
|
63747
65172
|
}
|
|
@@ -63764,7 +65189,7 @@ function parseGrokPermissionArrays(permission) {
|
|
|
63764
65189
|
const record = ({ category, pattern, action }) => {
|
|
63765
65190
|
const bucket = result[category] ??= {};
|
|
63766
65191
|
const existing = bucket[pattern];
|
|
63767
|
-
if (existing === void 0 || ACTION_RANK$
|
|
65192
|
+
if (existing === void 0 || ACTION_RANK$4[action] > ACTION_RANK$4[existing]) bucket[pattern] = action;
|
|
63768
65193
|
};
|
|
63769
65194
|
const applyEntries = (entries, action) => {
|
|
63770
65195
|
for (const entry of entries ?? []) {
|
|
@@ -64168,7 +65593,7 @@ const JUNIE_GROUP_TO_CANONICAL = {
|
|
|
64168
65593
|
mcpTools: "mcp",
|
|
64169
65594
|
readOutsideProject: "read"
|
|
64170
65595
|
};
|
|
64171
|
-
function isPermissionAction$
|
|
65596
|
+
function isPermissionAction$3(value) {
|
|
64172
65597
|
return PermissionActionSchema.safeParse(value).success;
|
|
64173
65598
|
}
|
|
64174
65599
|
/**
|
|
@@ -64354,7 +65779,7 @@ function convertJunieToRulesyncPermissions({ allowlist }) {
|
|
|
64354
65779
|
for (const rule of Array.isArray(ruleSet.rules) ? ruleSet.rules : []) {
|
|
64355
65780
|
if (!rule || typeof rule !== "object") continue;
|
|
64356
65781
|
const pattern = typeof rule.pattern === "string" ? rule.pattern : typeof rule.prefix === "string" ? rule.prefix : void 0;
|
|
64357
|
-
if (pattern === void 0 || !isPermissionAction$
|
|
65782
|
+
if (pattern === void 0 || !isPermissionAction$3(rule.action)) continue;
|
|
64358
65783
|
(permission[category] ??= {})[pattern] = rule.action;
|
|
64359
65784
|
}
|
|
64360
65785
|
}
|
|
@@ -65314,7 +66739,7 @@ function rulesFromArrays(settings, allowKey, denyKey) {
|
|
|
65314
66739
|
}
|
|
65315
66740
|
//#endregion
|
|
65316
66741
|
//#region src/features/permissions/kiro-capability-permissions.ts
|
|
65317
|
-
const CATCH_ALL_PATTERN$
|
|
66742
|
+
const CATCH_ALL_PATTERN$7 = "*";
|
|
65318
66743
|
const CANONICAL_TO_KIRO_CAPABILITY = {
|
|
65319
66744
|
bash: "shell",
|
|
65320
66745
|
read: "fs_read",
|
|
@@ -65331,12 +66756,12 @@ const KIRO_CAPABILITY_TO_CANONICAL = {
|
|
|
65331
66756
|
web_search: "websearch"
|
|
65332
66757
|
};
|
|
65333
66758
|
const MANAGED_CAPABILITIES = new Set(Object.keys(KIRO_CAPABILITY_TO_CANONICAL));
|
|
65334
|
-
const ACTION_RANK$
|
|
66759
|
+
const ACTION_RANK$3 = {
|
|
65335
66760
|
allow: 0,
|
|
65336
66761
|
ask: 1,
|
|
65337
66762
|
deny: 2
|
|
65338
66763
|
};
|
|
65339
|
-
function isPermissionAction$
|
|
66764
|
+
function isPermissionAction$2(value) {
|
|
65340
66765
|
return value === "allow" || value === "ask" || value === "deny";
|
|
65341
66766
|
}
|
|
65342
66767
|
/**
|
|
@@ -65363,7 +66788,7 @@ function rulePatternKey(capability, pattern) {
|
|
|
65363
66788
|
function classifyExistingEntry({ emitted, capability, pattern, effect }) {
|
|
65364
66789
|
const effects = emitted.get(rulePatternKey(capability, pattern));
|
|
65365
66790
|
if (effects === void 0) return "unrelated";
|
|
65366
|
-
return isPermissionAction$
|
|
66791
|
+
return isPermissionAction$2(effect) && effects.has(effect) ? "duplicate" : "conflict";
|
|
65367
66792
|
}
|
|
65368
66793
|
/**
|
|
65369
66794
|
* Merge one existing rule against the generated entries (see
|
|
@@ -65378,7 +66803,7 @@ function mergeExistingRule({ rule, emitted, logger }) {
|
|
|
65378
66803
|
};
|
|
65379
66804
|
const capability = rule.capability;
|
|
65380
66805
|
const isCatchAll = rule.match === void 0;
|
|
65381
|
-
const patterns = isStringArray$1(rule.match) ? rule.match : [CATCH_ALL_PATTERN$
|
|
66806
|
+
const patterns = isStringArray$1(rule.match) ? rule.match : [CATCH_ALL_PATTERN$7];
|
|
65382
66807
|
if (patterns.length === 0) return {
|
|
65383
66808
|
rule,
|
|
65384
66809
|
unrelatedEntries: 0
|
|
@@ -65432,7 +66857,7 @@ function mergeExistingRule({ rule, emitted, logger }) {
|
|
|
65432
66857
|
*/
|
|
65433
66858
|
function mergeKiroCapabilityRules({ existingRules, generatedRules, logger }) {
|
|
65434
66859
|
const emitted = /* @__PURE__ */ new Map();
|
|
65435
|
-
for (const rule of generatedRules) for (const pattern of rule.match ?? [CATCH_ALL_PATTERN$
|
|
66860
|
+
for (const rule of generatedRules) for (const pattern of rule.match ?? [CATCH_ALL_PATTERN$7]) {
|
|
65436
66861
|
const key = rulePatternKey(rule.capability, pattern);
|
|
65437
66862
|
const effects = emitted.get(key) ?? /* @__PURE__ */ new Set();
|
|
65438
66863
|
effects.add(rule.effect);
|
|
@@ -65560,13 +66985,13 @@ var KiroCapabilityPermissions = class KiroCapabilityPermissions extends ToolPerm
|
|
|
65560
66985
|
const rules = isRecord$1(parsed) && Array.isArray(parsed.rules) ? parsed.rules : [];
|
|
65561
66986
|
const permission = {};
|
|
65562
66987
|
for (const rule of rules) {
|
|
65563
|
-
if (!isImportableRule(rule) || !isRecord$1(rule) || !isPermissionAction$
|
|
66988
|
+
if (!isImportableRule(rule) || !isRecord$1(rule) || !isPermissionAction$2(rule.effect)) continue;
|
|
65564
66989
|
const category = KIRO_CAPABILITY_TO_CANONICAL[rule.capability];
|
|
65565
|
-
const patterns = rule.match === void 0 ? [CATCH_ALL_PATTERN$
|
|
66990
|
+
const patterns = rule.match === void 0 ? [CATCH_ALL_PATTERN$7] : isStringArray$1(rule.match) ? rule.match : [];
|
|
65566
66991
|
const categoryRules = permission[category] ??= {};
|
|
65567
66992
|
for (const pattern of patterns) {
|
|
65568
66993
|
const previous = categoryRules[pattern];
|
|
65569
|
-
if (previous === void 0 || ACTION_RANK$
|
|
66994
|
+
if (previous === void 0 || ACTION_RANK$3[rule.effect] > ACTION_RANK$3[previous]) categoryRules[pattern] = rule.effect;
|
|
65570
66995
|
}
|
|
65571
66996
|
}
|
|
65572
66997
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
|
|
@@ -65620,11 +67045,11 @@ function buildKiroCapabilityRules({ config, logger }) {
|
|
|
65620
67045
|
}
|
|
65621
67046
|
const result = [];
|
|
65622
67047
|
for (const { capability, effect, patterns } of buckets.values()) {
|
|
65623
|
-
if (patterns.has(CATCH_ALL_PATTERN$
|
|
67048
|
+
if (patterns.has(CATCH_ALL_PATTERN$7)) result.push({
|
|
65624
67049
|
capability,
|
|
65625
67050
|
effect
|
|
65626
67051
|
});
|
|
65627
|
-
const match = [...patterns].filter((pattern) => pattern !== CATCH_ALL_PATTERN$
|
|
67052
|
+
const match = [...patterns].filter((pattern) => pattern !== CATCH_ALL_PATTERN$7);
|
|
65628
67053
|
if (match.length > 0) result.push({
|
|
65629
67054
|
capability,
|
|
65630
67055
|
match,
|
|
@@ -65649,7 +67074,7 @@ function serializeKiroPermissions({ next, parsed, existingContent }) {
|
|
|
65649
67074
|
*/
|
|
65650
67075
|
function warnOnWorkspaceRelativeFsPattern({ capability, category, pattern, logger }) {
|
|
65651
67076
|
if (capability !== "fs_read" && capability !== "fs_write") return;
|
|
65652
|
-
if (pattern === CATCH_ALL_PATTERN$
|
|
67077
|
+
if (pattern === CATCH_ALL_PATTERN$7 || LOCATION_INDEPENDENT_FS_PATTERN.test(pattern)) return;
|
|
65653
67078
|
logger?.warn(`Kiro permissions.yaml is user-level and applies to every workspace, but the ${category} pattern "${pattern}" is relative. Use an absolute, ~-relative or **-prefixed pattern to target a specific location.`);
|
|
65654
67079
|
}
|
|
65655
67080
|
/**
|
|
@@ -65677,7 +67102,7 @@ const KiroScopedPermissions = {
|
|
|
65677
67102
|
//#region src/features/permissions/lettacode-permissions.ts
|
|
65678
67103
|
/** Top-level key of `.letta/settings.json` that rulesync owns. */
|
|
65679
67104
|
const LETTACODE_PERMISSIONS_KEY = "permissions";
|
|
65680
|
-
const CATCH_ALL_PATTERN$
|
|
67105
|
+
const CATCH_ALL_PATTERN$6 = "*";
|
|
65681
67106
|
/** Letta Code's `Bash(prefix:*)` prefix-match suffix. */
|
|
65682
67107
|
const PREFIX_SUFFIX = ":*";
|
|
65683
67108
|
const CATEGORY_TO_LETTACODE_TOOL = {
|
|
@@ -65722,12 +67147,12 @@ const LIST_TO_ACTION = {
|
|
|
65722
67147
|
alwaysAsk: "ask",
|
|
65723
67148
|
deny: "deny"
|
|
65724
67149
|
};
|
|
65725
|
-
const ACTION_RANK$
|
|
67150
|
+
const ACTION_RANK$2 = {
|
|
65726
67151
|
allow: 0,
|
|
65727
67152
|
ask: 1,
|
|
65728
67153
|
deny: 2
|
|
65729
67154
|
};
|
|
65730
|
-
function toStringList$
|
|
67155
|
+
function toStringList$2(value) {
|
|
65731
67156
|
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
65732
67157
|
}
|
|
65733
67158
|
/**
|
|
@@ -65770,7 +67195,7 @@ function categoryOfRule$1(rule) {
|
|
|
65770
67195
|
* widening it would grant commands the source never allowed.
|
|
65771
67196
|
*/
|
|
65772
67197
|
function toLettacodeBashPayload({ pattern, action, logger }) {
|
|
65773
|
-
if (pattern === CATCH_ALL_PATTERN$
|
|
67198
|
+
if (pattern === CATCH_ALL_PATTERN$6) return "";
|
|
65774
67199
|
const firstStar = pattern.indexOf("*");
|
|
65775
67200
|
if (firstStar === -1) return pattern;
|
|
65776
67201
|
if (pattern.endsWith(PREFIX_SUFFIX) && firstStar === pattern.length - 1) return pattern;
|
|
@@ -65791,7 +67216,7 @@ function toLettacodeRule({ category, pattern, action, logger }) {
|
|
|
65791
67216
|
pattern,
|
|
65792
67217
|
action,
|
|
65793
67218
|
logger
|
|
65794
|
-
}) : pattern === CATCH_ALL_PATTERN$
|
|
67219
|
+
}) : pattern === CATCH_ALL_PATTERN$6 ? "" : pattern;
|
|
65795
67220
|
if (payload === null) return null;
|
|
65796
67221
|
return payload === "" ? tool : `${tool}(${payload})`;
|
|
65797
67222
|
}
|
|
@@ -65802,7 +67227,7 @@ function fromLettacodeRule(rule) {
|
|
|
65802
67227
|
const { payload } = splitLettacodeRule(rule);
|
|
65803
67228
|
if (payload === void 0) return {
|
|
65804
67229
|
category,
|
|
65805
|
-
pattern: CATCH_ALL_PATTERN$
|
|
67230
|
+
pattern: CATCH_ALL_PATTERN$6
|
|
65806
67231
|
};
|
|
65807
67232
|
const trimmed = payload.trim();
|
|
65808
67233
|
if (trimmed === "") return null;
|
|
@@ -65810,7 +67235,7 @@ function fromLettacodeRule(rule) {
|
|
|
65810
67235
|
const prefix = trimmed.slice(0, -2).trimEnd();
|
|
65811
67236
|
return {
|
|
65812
67237
|
category,
|
|
65813
|
-
pattern: prefix === "" ? CATCH_ALL_PATTERN$
|
|
67238
|
+
pattern: prefix === "" ? CATCH_ALL_PATTERN$6 : `${prefix} *`
|
|
65814
67239
|
};
|
|
65815
67240
|
}
|
|
65816
67241
|
return {
|
|
@@ -65845,11 +67270,11 @@ function buildLettacodeRuleLists({ config, existingPermissions, logger }) {
|
|
|
65845
67270
|
});
|
|
65846
67271
|
if (rule === null) continue;
|
|
65847
67272
|
const existing = ranked.get(rule);
|
|
65848
|
-
if (existing === void 0 || ACTION_RANK$
|
|
67273
|
+
if (existing === void 0 || ACTION_RANK$2[action] > ACTION_RANK$2[existing]) ranked.set(rule, action);
|
|
65849
67274
|
}
|
|
65850
67275
|
}
|
|
65851
67276
|
const lists = {};
|
|
65852
|
-
for (const key of Object.keys(LIST_TO_ACTION)) lists[key] = toStringList$
|
|
67277
|
+
for (const key of Object.keys(LIST_TO_ACTION)) lists[key] = toStringList$2(existingPermissions[key]).filter((rule) => {
|
|
65853
67278
|
const category = categoryOfRule$1(rule);
|
|
65854
67279
|
return category === void 0 || !managedCategories.has(category);
|
|
65855
67280
|
});
|
|
@@ -65938,12 +67363,12 @@ var LettacodePermissions = class LettacodePermissions extends ToolPermissions {
|
|
|
65938
67363
|
});
|
|
65939
67364
|
const permissions = isRecord$1(settings[LETTACODE_PERMISSIONS_KEY]) ? settings[LETTACODE_PERMISSIONS_KEY] : {};
|
|
65940
67365
|
const permission = {};
|
|
65941
|
-
for (const [key, action] of Object.entries(LIST_TO_ACTION)) for (const rule of toStringList$
|
|
67366
|
+
for (const [key, action] of Object.entries(LIST_TO_ACTION)) for (const rule of toStringList$2(permissions[key])) {
|
|
65942
67367
|
const parsed = fromLettacodeRule(rule);
|
|
65943
67368
|
if (parsed === null || isPrototypePollutionKey(parsed.pattern)) continue;
|
|
65944
67369
|
const rules = permission[parsed.category] ??= {};
|
|
65945
67370
|
const existing = rules[parsed.pattern];
|
|
65946
|
-
if (existing === void 0 || ACTION_RANK$
|
|
67371
|
+
if (existing === void 0 || ACTION_RANK$2[action] > ACTION_RANK$2[existing]) rules[parsed.pattern] = action;
|
|
65947
67372
|
}
|
|
65948
67373
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
|
|
65949
67374
|
}
|
|
@@ -66127,7 +67552,11 @@ var OpencodePermissions = class extends ToolPermissions {
|
|
|
66127
67552
|
fileContent = await readFileContentOrNull(jsonPath);
|
|
66128
67553
|
if (fileContent) relativeFilePath = this.layout.jsonFileName;
|
|
66129
67554
|
}
|
|
66130
|
-
const parsed = (
|
|
67555
|
+
const parsed = (await readOpencodeDotDirConfigLayers({
|
|
67556
|
+
outputRoot,
|
|
67557
|
+
global,
|
|
67558
|
+
layout: this.layout
|
|
67559
|
+
})).map(({ config }) => config).reduce(mergeOpencodeConfigValues, (0, jsonc_parser.parse)(fileContent ?? "{}"));
|
|
66131
67560
|
const record = isRecord$1(parsed) ? parsed : {};
|
|
66132
67561
|
const nextJson = {
|
|
66133
67562
|
...record,
|
|
@@ -66178,6 +67607,16 @@ var OpencodePermissions = class extends ToolPermissions {
|
|
|
66178
67607
|
value,
|
|
66179
67608
|
logger
|
|
66180
67609
|
});
|
|
67610
|
+
warnOpencodeDotDirOverrides({
|
|
67611
|
+
generated: { permission },
|
|
67612
|
+
overlays: await readOpencodeDotDirConfigLayers({
|
|
67613
|
+
outputRoot,
|
|
67614
|
+
global,
|
|
67615
|
+
layout: this.layout
|
|
67616
|
+
}),
|
|
67617
|
+
rootFileName: relativeFilePath,
|
|
67618
|
+
logger
|
|
67619
|
+
});
|
|
66181
67620
|
return new this({
|
|
66182
67621
|
outputRoot,
|
|
66183
67622
|
relativeDirPath: basePaths.relativeDirPath,
|
|
@@ -66267,7 +67706,7 @@ var MimocodePermissions = class extends OpencodePermissions {
|
|
|
66267
67706
|
};
|
|
66268
67707
|
//#endregion
|
|
66269
67708
|
//#region src/features/permissions/omp-permissions.ts
|
|
66270
|
-
const CATCH_ALL_PATTERN$
|
|
67709
|
+
const CATCH_ALL_PATTERN$5 = "*";
|
|
66271
67710
|
const APPROVAL_MODE_KEY = "approvalMode";
|
|
66272
67711
|
const ACTION_TO_OMP_APPROVAL = {
|
|
66273
67712
|
allow: "allow",
|
|
@@ -66464,7 +67903,7 @@ function allToolsFloor(rules) {
|
|
|
66464
67903
|
let patternSpecific = false;
|
|
66465
67904
|
for (const [pattern, action] of Object.entries(rules ?? {})) {
|
|
66466
67905
|
if (action === "allow" || isPrototypePollutionKey(pattern)) continue;
|
|
66467
|
-
if (pattern === CATCH_ALL_PATTERN$
|
|
67906
|
+
if (pattern === CATCH_ALL_PATTERN$5) floor = stricter(floor, action);
|
|
66468
67907
|
else {
|
|
66469
67908
|
patternSpecific = true;
|
|
66470
67909
|
floor = stricter(floor, "ask");
|
|
@@ -66499,11 +67938,11 @@ function convertBashRules$1({ rules, logger }) {
|
|
|
66499
67938
|
const entries = Object.entries(rules).filter(([pattern]) => !isPrototypePollutionKey(pattern));
|
|
66500
67939
|
const hasAsk = entries.some(([, action]) => action === "ask");
|
|
66501
67940
|
let policy = hasAsk ? "ask" : void 0;
|
|
66502
|
-
if (hasAsk && rules[CATCH_ALL_PATTERN$
|
|
67941
|
+
if (hasAsk && rules[CATCH_ALL_PATTERN$5] !== "ask") logger?.warn("oh-my-pi lets a critical command (such as `rm -rf /`) skip a bash prompt pattern unless the bash tool policy prompts, so `tools.approval.bash` was set to \"prompt\": a command no bash allow pattern matches now prompts in every approval mode.");
|
|
66503
67942
|
for (const [pattern, action] of entries) {
|
|
66504
|
-
if (pattern === CATCH_ALL_PATTERN$
|
|
67943
|
+
if (pattern === CATCH_ALL_PATTERN$5 && action === "allow") {
|
|
66505
67944
|
if (hasAsk) patterns.push({
|
|
66506
|
-
match: CATCH_ALL_PATTERN$
|
|
67945
|
+
match: CATCH_ALL_PATTERN$5,
|
|
66507
67946
|
approval: "allow"
|
|
66508
67947
|
});
|
|
66509
67948
|
else policy = "allow";
|
|
@@ -66606,12 +68045,12 @@ function toOmpApproval(value) {
|
|
|
66606
68045
|
* ask rules (see `convertBashRules`), so it adds no catch-all.
|
|
66607
68046
|
*/
|
|
66608
68047
|
function importBashPolicy(bashRules, action) {
|
|
66609
|
-
const catchAll = bashRules[CATCH_ALL_PATTERN$
|
|
68048
|
+
const catchAll = bashRules[CATCH_ALL_PATTERN$5];
|
|
66610
68049
|
const hasAskPattern = Object.values(bashRules).includes("ask");
|
|
66611
|
-
if (action === "deny") bashRules[CATCH_ALL_PATTERN$
|
|
68050
|
+
if (action === "deny") bashRules[CATCH_ALL_PATTERN$5] = "deny";
|
|
66612
68051
|
else if (catchAll === void 0) {
|
|
66613
|
-
if (action !== void 0 && !(action === "ask" && hasAskPattern)) bashRules[CATCH_ALL_PATTERN$
|
|
66614
|
-
} else if (catchAll === "allow" && action !== "allow" && !(action === "ask" && hasAskPattern)) bashRules[CATCH_ALL_PATTERN$
|
|
68052
|
+
if (action !== void 0 && !(action === "ask" && hasAskPattern)) bashRules[CATCH_ALL_PATTERN$5] = action;
|
|
68053
|
+
} else if (catchAll === "allow" && action !== "allow" && !(action === "ask" && hasAskPattern)) bashRules[CATCH_ALL_PATTERN$5] = "ask";
|
|
66615
68054
|
}
|
|
66616
68055
|
/** `bash.patterns` as canonical `bash` rules, in first-match order. */
|
|
66617
68056
|
function importBashPatterns(value) {
|
|
@@ -66639,7 +68078,7 @@ function convertOmpToRulesync(config) {
|
|
|
66639
68078
|
const action = OMP_APPROVAL_TO_ACTION[approval];
|
|
66640
68079
|
if (tool === "bash") continue;
|
|
66641
68080
|
const category = renamed(OMP_TOOL_TO_CATEGORY, tool);
|
|
66642
|
-
permission[category] = { [CATCH_ALL_PATTERN$
|
|
68081
|
+
permission[category] = { [CATCH_ALL_PATTERN$5]: stricter(permission[category]?.[CATCH_ALL_PATTERN$5], action) };
|
|
66643
68082
|
}
|
|
66644
68083
|
if (Object.keys(bashRules).length > 0) permission.bash = bashRules;
|
|
66645
68084
|
const approvalMode = tools[APPROVAL_MODE_KEY];
|
|
@@ -66769,7 +68208,7 @@ const POOL_DENY_KEY = "deny";
|
|
|
66769
68208
|
const POOL_DISABLED_KEY = "disabled";
|
|
66770
68209
|
const POOL_PATH_KEY = "path";
|
|
66771
68210
|
const POOL_WRITE_KEY = "write";
|
|
66772
|
-
const CATCH_ALL_PATTERN$
|
|
68211
|
+
const CATCH_ALL_PATTERN$4 = "*";
|
|
66773
68212
|
const PATHS_CATCH_ALL_PATTERN = "**";
|
|
66774
68213
|
const CANONICAL_TO_POOL_TOOL_NAMES = {
|
|
66775
68214
|
bash: "shell",
|
|
@@ -67277,7 +68716,7 @@ function pathOfEntry(entry) {
|
|
|
67277
68716
|
*/
|
|
67278
68717
|
function importPoolTool({ toolConfig, bucket }) {
|
|
67279
68718
|
if (toolConfig[POOL_DISABLED_KEY] === true) {
|
|
67280
|
-
bucket[CATCH_ALL_PATTERN$
|
|
68719
|
+
bucket[CATCH_ALL_PATTERN$4] = "deny";
|
|
67281
68720
|
return;
|
|
67282
68721
|
}
|
|
67283
68722
|
const denied = isStringArray$1(toolConfig[POOL_DENY_KEY]) ? toolConfig[POOL_DENY_KEY] : [];
|
|
@@ -67334,9 +68773,58 @@ const QODER_PERMISSIONS_KEY = "permissions";
|
|
|
67334
68773
|
* @see https://docs.qoder.com/cli/settings-reference
|
|
67335
68774
|
*/
|
|
67336
68775
|
const QODER_OUTPUT_STYLE_KEY = "outputStyle";
|
|
67337
|
-
|
|
68776
|
+
/**
|
|
68777
|
+
* The settings groups the `qoder` override may author, each with the only keys
|
|
68778
|
+
* of that group rulesync writes and imports. A group's other keys are kept as
|
|
68779
|
+
* the file has them; an override key not listed here is skipped with a
|
|
68780
|
+
* warning, so a shared permissions file cannot reach `tools.shell.pager`,
|
|
68781
|
+
* `general.preferredEditor` and the like.
|
|
68782
|
+
* @see https://docs.qoder.com/cli/settings-reference
|
|
68783
|
+
*/
|
|
68784
|
+
const QODER_OVERRIDE_GROUP_KEYS = {
|
|
68785
|
+
general: ["defaultPermissionMode"],
|
|
68786
|
+
tools: ["core", "exclude"],
|
|
68787
|
+
mcp: [
|
|
68788
|
+
"allowed",
|
|
68789
|
+
"excluded",
|
|
68790
|
+
"enableAllProjectMcpServers",
|
|
68791
|
+
"enabledProjectMcpServers",
|
|
68792
|
+
"lazyLoad"
|
|
68793
|
+
],
|
|
68794
|
+
agents: ["overrides"]
|
|
68795
|
+
};
|
|
68796
|
+
const QODER_OVERRIDE_GROUPS = Object.keys(QODER_OVERRIDE_GROUP_KEYS);
|
|
68797
|
+
/**
|
|
68798
|
+
* Top-level Auto-mode classifier guidance. Qoder reads it from the user
|
|
68799
|
+
* settings only (a workspace file could otherwise bias the classifier), so it
|
|
68800
|
+
* is written and imported in global mode alone.
|
|
68801
|
+
* @see https://docs.qoder.com/en/cli/permissions
|
|
68802
|
+
*/
|
|
68803
|
+
const QODER_AUTO_MODE_KEY = "autoMode";
|
|
68804
|
+
/** The `autoMode` keys rulesync writes and imports. */
|
|
68805
|
+
const QODER_AUTO_MODE_KEYS = [
|
|
68806
|
+
"allow",
|
|
68807
|
+
"soft_deny",
|
|
68808
|
+
"environment"
|
|
68809
|
+
];
|
|
68810
|
+
/** Top-level globs that keep project/local memory files from loading. */
|
|
68811
|
+
const QODER_AGENTS_MD_EXCLUDES_KEY = "agentsMdExcludes";
|
|
68812
|
+
/**
|
|
68813
|
+
* `general.defaultPermissionMode` values (lower-cased; Qoder matches them
|
|
68814
|
+
* case-insensitively) that let actions run without a prompt.
|
|
68815
|
+
* @see https://docs.qoder.com/en/cli/permissions
|
|
68816
|
+
*/
|
|
68817
|
+
const QODER_PERMISSIVE_MODES = /* @__PURE__ */ new Set([
|
|
68818
|
+
"accept_edits",
|
|
68819
|
+
"acceptedits",
|
|
68820
|
+
"auto",
|
|
68821
|
+
"bypass_permissions",
|
|
68822
|
+
"bypasspermissions",
|
|
68823
|
+
"yolo"
|
|
68824
|
+
]);
|
|
68825
|
+
const CATCH_ALL_PATTERN$3 = "*";
|
|
67338
68826
|
/** Prefix of the fully qualified MCP tool names (`mcp__<server>__<tool>`). */
|
|
67339
|
-
const MCP_TOOL_PREFIX = "mcp__";
|
|
68827
|
+
const MCP_TOOL_PREFIX$1 = "mcp__";
|
|
67340
68828
|
const CATEGORY_TO_QODER_TOOL = {
|
|
67341
68829
|
bash: "Bash",
|
|
67342
68830
|
read: "Read",
|
|
@@ -67366,12 +68854,12 @@ const ACTIONS = [
|
|
|
67366
68854
|
"ask",
|
|
67367
68855
|
"deny"
|
|
67368
68856
|
];
|
|
67369
|
-
const ACTION_RANK = {
|
|
68857
|
+
const ACTION_RANK$1 = {
|
|
67370
68858
|
allow: 0,
|
|
67371
68859
|
ask: 1,
|
|
67372
68860
|
deny: 2
|
|
67373
68861
|
};
|
|
67374
|
-
function toStringList(value) {
|
|
68862
|
+
function toStringList$1(value) {
|
|
67375
68863
|
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
67376
68864
|
}
|
|
67377
68865
|
/**
|
|
@@ -67393,7 +68881,7 @@ function splitQoderRule(rule) {
|
|
|
67393
68881
|
function categoryOfRule(rule) {
|
|
67394
68882
|
const { tool } = splitQoderRule(rule);
|
|
67395
68883
|
if (tool === null) return;
|
|
67396
|
-
if (tool === "*" || tool.startsWith(MCP_TOOL_PREFIX)) return tool;
|
|
68884
|
+
if (tool === "*" || tool.startsWith(MCP_TOOL_PREFIX$1)) return tool;
|
|
67397
68885
|
return Object.hasOwn(QODER_TOOL_TO_CATEGORY, tool) ? QODER_TOOL_TO_CATEGORY[tool] : void 0;
|
|
67398
68886
|
}
|
|
67399
68887
|
/**
|
|
@@ -67412,18 +68900,18 @@ function categoryOfRule(rule) {
|
|
|
67412
68900
|
*/
|
|
67413
68901
|
function toQoderRule({ category, pattern, logger }) {
|
|
67414
68902
|
if (category === "*") {
|
|
67415
|
-
if (pattern === CATCH_ALL_PATTERN$
|
|
68903
|
+
if (pattern === CATCH_ALL_PATTERN$3) return CATCH_ALL_PATTERN$3;
|
|
67416
68904
|
logger?.warn(`Qoder permissions: skipping the all-tools '*' rule '${pattern}' — a Qoder rule with a payload names one tool. Write it under the categories it is meant for (for example 'bash' or 'read').`);
|
|
67417
68905
|
return null;
|
|
67418
68906
|
}
|
|
67419
|
-
if (category.startsWith(MCP_TOOL_PREFIX)) {
|
|
67420
|
-
if (pattern === CATCH_ALL_PATTERN$
|
|
68907
|
+
if (category.startsWith(MCP_TOOL_PREFIX$1)) {
|
|
68908
|
+
if (pattern === CATCH_ALL_PATTERN$3) return category;
|
|
67421
68909
|
logger?.warn(`Qoder permissions: skipping the '${category}' rule '${pattern}' — Qoder matches MCP tools by name only.`);
|
|
67422
68910
|
return null;
|
|
67423
68911
|
}
|
|
67424
68912
|
const tool = CATEGORY_TO_QODER_TOOL[category];
|
|
67425
68913
|
if (tool === void 0) return null;
|
|
67426
|
-
if (pattern === CATCH_ALL_PATTERN$
|
|
68914
|
+
if (pattern === CATCH_ALL_PATTERN$3) return tool;
|
|
67427
68915
|
return `${QODER_PATH_RULE_ALIASES[tool] ?? tool}(${pattern})`;
|
|
67428
68916
|
}
|
|
67429
68917
|
/** Read a Qoder rule back into a canonical category and pattern. */
|
|
@@ -67433,9 +68921,9 @@ function fromQoderRule(rule) {
|
|
|
67433
68921
|
const { payload } = splitQoderRule(rule);
|
|
67434
68922
|
if (payload === void 0) return {
|
|
67435
68923
|
category,
|
|
67436
|
-
pattern: CATCH_ALL_PATTERN$
|
|
68924
|
+
pattern: CATCH_ALL_PATTERN$3
|
|
67437
68925
|
};
|
|
67438
|
-
if (category === "*" || category.startsWith(MCP_TOOL_PREFIX)) return null;
|
|
68926
|
+
if (category === "*" || category.startsWith(MCP_TOOL_PREFIX$1)) return null;
|
|
67439
68927
|
const trimmed = payload.trim();
|
|
67440
68928
|
if (trimmed === "") return null;
|
|
67441
68929
|
return {
|
|
@@ -67455,7 +68943,7 @@ function buildQoderRuleLists({ config, existingPermissions, logger }) {
|
|
|
67455
68943
|
const ranked = /* @__PURE__ */ new Map();
|
|
67456
68944
|
const managedCategories = /* @__PURE__ */ new Set();
|
|
67457
68945
|
for (const [category, rules] of Object.entries(permission)) {
|
|
67458
|
-
if (category !== "*" && !category.startsWith(MCP_TOOL_PREFIX) && !Object.hasOwn(CATEGORY_TO_QODER_TOOL, category)) {
|
|
68946
|
+
if (category !== "*" && !category.startsWith(MCP_TOOL_PREFIX$1) && !Object.hasOwn(CATEGORY_TO_QODER_TOOL, category)) {
|
|
67459
68947
|
logger?.warn(`Qoder permissions: skipping the '${category}' category — Qoder has no permission rule for it.`);
|
|
67460
68948
|
continue;
|
|
67461
68949
|
}
|
|
@@ -67469,11 +68957,11 @@ function buildQoderRuleLists({ config, existingPermissions, logger }) {
|
|
|
67469
68957
|
});
|
|
67470
68958
|
if (rule === null) continue;
|
|
67471
68959
|
const existing = ranked.get(rule);
|
|
67472
|
-
if (existing === void 0 || ACTION_RANK[action] > ACTION_RANK[existing]) ranked.set(rule, action);
|
|
68960
|
+
if (existing === void 0 || ACTION_RANK$1[action] > ACTION_RANK$1[existing]) ranked.set(rule, action);
|
|
67473
68961
|
}
|
|
67474
68962
|
}
|
|
67475
68963
|
const lists = {};
|
|
67476
|
-
for (const action of ACTIONS) lists[action] = toStringList(existingPermissions[action]).filter((rule) => {
|
|
68964
|
+
for (const action of ACTIONS) lists[action] = toStringList$1(existingPermissions[action]).filter((rule) => {
|
|
67477
68965
|
if (ranked.has(rule)) return false;
|
|
67478
68966
|
const category = categoryOfRule(rule);
|
|
67479
68967
|
return category === void 0 || !managedCategories.has(category);
|
|
@@ -67483,6 +68971,130 @@ function buildQoderRuleLists({ config, existingPermissions, logger }) {
|
|
|
67483
68971
|
return lists;
|
|
67484
68972
|
}
|
|
67485
68973
|
/**
|
|
68974
|
+
* The keys of one authored override group that rulesync writes; any other key
|
|
68975
|
+
* is skipped with a warning.
|
|
68976
|
+
*/
|
|
68977
|
+
function pickQoderGroupKeys({ group, allowedKeys, authored, where, logger }) {
|
|
68978
|
+
const picked = {};
|
|
68979
|
+
for (const [key, value] of Object.entries(authored)) {
|
|
68980
|
+
if (isPrototypePollutionKey(key) || value === void 0) continue;
|
|
68981
|
+
if (!allowedKeys.includes(key)) {
|
|
68982
|
+
logger?.warn(`Qoder permissions: skipping qoder.${group}.${key} — Rulesync writes only ${allowedKeys.map((k) => `${group}.${k}`).join(", ")} from the qoder override; set other keys in ${where} by hand.`);
|
|
68983
|
+
continue;
|
|
68984
|
+
}
|
|
68985
|
+
picked[key] = value;
|
|
68986
|
+
}
|
|
68987
|
+
return picked;
|
|
68988
|
+
}
|
|
68989
|
+
/** Name every written override setting that lets Qoder act with fewer prompts. */
|
|
68990
|
+
function announcePermissiveQoderSettings({ patch, where, logger }) {
|
|
68991
|
+
const mode = (isRecord$1(patch.general) ? patch.general : {}).defaultPermissionMode;
|
|
68992
|
+
if (typeof mode === "string" && QODER_PERMISSIVE_MODES.has(mode.trim().toLowerCase())) logger?.warn(`Qoder permissions: qoder.general.defaultPermissionMode = ${JSON.stringify(mode)} was written to ${where}, so Qoder starts every session it applies to in a mode that runs actions without asking first.`);
|
|
68993
|
+
const mcp = isRecord$1(patch.mcp) ? patch.mcp : {};
|
|
68994
|
+
if (mcp.enableAllProjectMcpServers === true) logger?.warn(`Qoder permissions: qoder.mcp.enableAllProjectMcpServers = true was written to ${where}, so Qoder starts every project-level MCP server without asking for approval.`);
|
|
68995
|
+
const enabledServers = toStringList$1(mcp.enabledProjectMcpServers);
|
|
68996
|
+
if (enabledServers.length > 0) logger?.warn(`Qoder permissions: qoder.mcp.enabledProjectMcpServers was written to ${where}, so Qoder starts the project-level MCP servers ${enabledServers.map((name) => JSON.stringify(name)).join(", ")} without asking for approval.`);
|
|
68997
|
+
if (toStringList$1((isRecord$1(patch[QODER_AUTO_MODE_KEY]) ? patch[QODER_AUTO_MODE_KEY] : {}).allow).length > 0) logger?.warn(`Qoder permissions: qoder.autoMode.allow was written to ${where}, so Qoder's Auto-mode classifier leans toward approving the actions it describes.`);
|
|
68998
|
+
}
|
|
68999
|
+
/**
|
|
69000
|
+
* The top-level keys the `qoder` override contributes to the settings patch,
|
|
69001
|
+
* besides `permissions` and `outputStyle`. Each group is the existing group
|
|
69002
|
+
* with the authored keys laid over it; a key is only in the patch when
|
|
69003
|
+
* authored. Every write that lets Qoder act with fewer prompts is announced.
|
|
69004
|
+
*/
|
|
69005
|
+
function buildQoderOverridePatch({ override, existing, global, filePath, logger }) {
|
|
69006
|
+
const patch = {};
|
|
69007
|
+
if (override === void 0) return patch;
|
|
69008
|
+
const where = `${filePath}${global ? " in the home directory" : ""}`;
|
|
69009
|
+
for (const group of QODER_OVERRIDE_GROUPS) {
|
|
69010
|
+
const authored = override[group];
|
|
69011
|
+
if (!isRecord$1(authored)) continue;
|
|
69012
|
+
const picked = pickQoderGroupKeys({
|
|
69013
|
+
group,
|
|
69014
|
+
allowedKeys: QODER_OVERRIDE_GROUP_KEYS[group],
|
|
69015
|
+
authored,
|
|
69016
|
+
where,
|
|
69017
|
+
logger
|
|
69018
|
+
});
|
|
69019
|
+
if (Object.keys(picked).length > 0) patch[group] = {
|
|
69020
|
+
...isRecord$1(existing[group]) ? existing[group] : {},
|
|
69021
|
+
...picked
|
|
69022
|
+
};
|
|
69023
|
+
}
|
|
69024
|
+
const autoMode = override[QODER_AUTO_MODE_KEY];
|
|
69025
|
+
if (isRecord$1(autoMode)) {
|
|
69026
|
+
if (global) {
|
|
69027
|
+
const picked = pickQoderGroupKeys({
|
|
69028
|
+
group: QODER_AUTO_MODE_KEY,
|
|
69029
|
+
allowedKeys: QODER_AUTO_MODE_KEYS,
|
|
69030
|
+
authored: autoMode,
|
|
69031
|
+
where,
|
|
69032
|
+
logger
|
|
69033
|
+
});
|
|
69034
|
+
if (Object.keys(picked).length > 0) {
|
|
69035
|
+
const existingAutoMode = isRecord$1(existing[QODER_AUTO_MODE_KEY]) ? existing[QODER_AUTO_MODE_KEY] : {};
|
|
69036
|
+
patch[QODER_AUTO_MODE_KEY] = {
|
|
69037
|
+
...existingAutoMode,
|
|
69038
|
+
...picked
|
|
69039
|
+
};
|
|
69040
|
+
}
|
|
69041
|
+
} else logger?.warn(`Qoder permissions: skipping qoder.autoMode in project mode — Qoder reads autoMode from the user settings only. Run 'rulesync generate --global' to write it to ~/.qoder/settings.json.`);
|
|
69042
|
+
}
|
|
69043
|
+
const excludes = override[QODER_AGENTS_MD_EXCLUDES_KEY];
|
|
69044
|
+
if (Array.isArray(excludes)) patch[QODER_AGENTS_MD_EXCLUDES_KEY] = toStringList$1(excludes);
|
|
69045
|
+
announcePermissiveQoderSettings({
|
|
69046
|
+
patch: {
|
|
69047
|
+
general: override.general,
|
|
69048
|
+
mcp: override.mcp,
|
|
69049
|
+
...global && { [QODER_AUTO_MODE_KEY]: autoMode }
|
|
69050
|
+
},
|
|
69051
|
+
where,
|
|
69052
|
+
logger
|
|
69053
|
+
});
|
|
69054
|
+
return patch;
|
|
69055
|
+
}
|
|
69056
|
+
/**
|
|
69057
|
+
* Whether a settings value has the shape the `qoder` override schema accepts
|
|
69058
|
+
* for that key, so an off-spec value is left in the file instead of making the
|
|
69059
|
+
* imported permissions file fail validation.
|
|
69060
|
+
*/
|
|
69061
|
+
function isLiftableQoderValue(key, value) {
|
|
69062
|
+
switch (key) {
|
|
69063
|
+
case "defaultPermissionMode": return typeof value === "string" && value.trim() !== "";
|
|
69064
|
+
case "enableAllProjectMcpServers":
|
|
69065
|
+
case "lazyLoad": return typeof value === "boolean";
|
|
69066
|
+
case "overrides": return isRecord$1(value) && Object.values(value).every(isRecord$1);
|
|
69067
|
+
default: return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
69068
|
+
}
|
|
69069
|
+
}
|
|
69070
|
+
/**
|
|
69071
|
+
* Lift the settings the `qoder` override authors (other than `outputStyle`)
|
|
69072
|
+
* back out of a settings file. `autoMode` is lifted only from the user file,
|
|
69073
|
+
* the one Qoder reads it from.
|
|
69074
|
+
*/
|
|
69075
|
+
function liftQoderOverride({ settings, global }) {
|
|
69076
|
+
const lifted = {};
|
|
69077
|
+
for (const group of QODER_OVERRIDE_GROUPS) {
|
|
69078
|
+
const existingGroup = settings[group];
|
|
69079
|
+
if (!isRecord$1(existingGroup)) continue;
|
|
69080
|
+
const picked = {};
|
|
69081
|
+
for (const key of QODER_OVERRIDE_GROUP_KEYS[group]) {
|
|
69082
|
+
const value = Object.hasOwn(existingGroup, key) ? existingGroup[key] : void 0;
|
|
69083
|
+
if (isLiftableQoderValue(key, value)) picked[key] = value;
|
|
69084
|
+
}
|
|
69085
|
+
if (Object.keys(picked).length > 0) lifted[group] = picked;
|
|
69086
|
+
}
|
|
69087
|
+
const autoMode = settings[QODER_AUTO_MODE_KEY];
|
|
69088
|
+
if (global && isRecord$1(autoMode)) {
|
|
69089
|
+
const picked = {};
|
|
69090
|
+
for (const key of QODER_AUTO_MODE_KEYS) if (Object.hasOwn(autoMode, key) && isLiftableQoderValue(key, autoMode[key])) picked[key] = autoMode[key];
|
|
69091
|
+
if (Object.keys(picked).length > 0) lifted[QODER_AUTO_MODE_KEY] = picked;
|
|
69092
|
+
}
|
|
69093
|
+
const excludes = settings[QODER_AGENTS_MD_EXCLUDES_KEY];
|
|
69094
|
+
if (isLiftableQoderValue(QODER_AGENTS_MD_EXCLUDES_KEY, excludes)) lifted[QODER_AGENTS_MD_EXCLUDES_KEY] = excludes;
|
|
69095
|
+
return lifted;
|
|
69096
|
+
}
|
|
69097
|
+
/**
|
|
67486
69098
|
* Qoder permissions.
|
|
67487
69099
|
*
|
|
67488
69100
|
* Rules live under the `permissions` key of `.qoder/settings.json` (project)
|
|
@@ -67494,7 +69106,9 @@ function buildQoderRuleLists({ config, existingPermissions, logger }) {
|
|
|
67494
69106
|
* (`additionalDirectories`, `trustDirectories`) are kept, and the file is
|
|
67495
69107
|
* never deleted. `.qoder/settings.local.json` (where `/allow` and `/deny`
|
|
67496
69108
|
* persist) is left to the user. The `qoder` override's `outputStyle` is
|
|
67497
|
-
* written to (and imported from) the top-level `outputStyle` key
|
|
69109
|
+
* written to (and imported from) the top-level `outputStyle` key, and its
|
|
69110
|
+
* other settings (`general`, `tools`, `mcp`, `agents`, `autoMode`,
|
|
69111
|
+
* `agentsMdExcludes`) through `buildQoderOverridePatch` / `liftQoderOverride`.
|
|
67498
69112
|
*
|
|
67499
69113
|
* @see https://docs.qoder.com/en/cli/permissions
|
|
67500
69114
|
* @see https://docs.qoder.com/cli/output-styles
|
|
@@ -67557,7 +69171,14 @@ var QoderPermissions = class QoderPermissions extends ToolPermissions {
|
|
|
67557
69171
|
existingContent,
|
|
67558
69172
|
patch: {
|
|
67559
69173
|
[QODER_PERMISSIONS_KEY]: Object.keys(permissions).length > 0 ? permissions : void 0,
|
|
67560
|
-
...outputStyle !== void 0 && { [QODER_OUTPUT_STYLE_KEY]: outputStyle }
|
|
69174
|
+
...outputStyle !== void 0 && { [QODER_OUTPUT_STYLE_KEY]: outputStyle },
|
|
69175
|
+
...buildQoderOverridePatch({
|
|
69176
|
+
override: config.qoder,
|
|
69177
|
+
existing,
|
|
69178
|
+
global,
|
|
69179
|
+
filePath: (0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath),
|
|
69180
|
+
logger
|
|
69181
|
+
})
|
|
67561
69182
|
},
|
|
67562
69183
|
filePath
|
|
67563
69184
|
}),
|
|
@@ -67572,18 +69193,24 @@ var QoderPermissions = class QoderPermissions extends ToolPermissions {
|
|
|
67572
69193
|
});
|
|
67573
69194
|
const permissions = isRecord$1(settings[QODER_PERMISSIONS_KEY]) ? settings[QODER_PERMISSIONS_KEY] : {};
|
|
67574
69195
|
const permission = {};
|
|
67575
|
-
for (const action of ACTIONS) for (const rule of toStringList(permissions[action])) {
|
|
69196
|
+
for (const action of ACTIONS) for (const rule of toStringList$1(permissions[action])) {
|
|
67576
69197
|
const parsed = fromQoderRule(rule);
|
|
67577
69198
|
if (parsed === null || isPrototypePollutionKey(parsed.category) || isPrototypePollutionKey(parsed.pattern)) continue;
|
|
67578
69199
|
const rules = permission[parsed.category] ??= {};
|
|
67579
69200
|
const existing = rules[parsed.pattern];
|
|
67580
|
-
if (existing === void 0 || ACTION_RANK[action] > ACTION_RANK[existing]) rules[parsed.pattern] = action;
|
|
69201
|
+
if (existing === void 0 || ACTION_RANK$1[action] > ACTION_RANK$1[existing]) rules[parsed.pattern] = action;
|
|
67581
69202
|
}
|
|
67582
69203
|
const outputStyle = settings[QODER_OUTPUT_STYLE_KEY];
|
|
67583
|
-
const
|
|
69204
|
+
const qoder = {
|
|
69205
|
+
...typeof outputStyle === "string" && outputStyle.trim() !== "" && { outputStyle },
|
|
69206
|
+
...liftQoderOverride({
|
|
69207
|
+
settings,
|
|
69208
|
+
global: this.global
|
|
69209
|
+
})
|
|
69210
|
+
};
|
|
67584
69211
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({
|
|
67585
69212
|
permission,
|
|
67586
|
-
...
|
|
69213
|
+
...Object.keys(qoder).length > 0 && { qoder }
|
|
67587
69214
|
}, null, 2) });
|
|
67588
69215
|
}
|
|
67589
69216
|
validate() {
|
|
@@ -68444,9 +70071,53 @@ const REASONIX_TRUST_AFFECTING_SANDBOX_PATHS = [
|
|
|
68444
70071
|
path: ["workspace_root"],
|
|
68445
70072
|
reason: "moves the root the file-writing tools and sandboxed Bash are confined to, so what they may reach is decided by this path rather than by the project directory",
|
|
68446
70073
|
widens: escapesTheProject
|
|
70074
|
+
},
|
|
70075
|
+
{
|
|
70076
|
+
path: ["allow_devices"],
|
|
70077
|
+
reason: "binds host device nodes into the Linux Bash sandbox, which otherwise sees a private /dev",
|
|
70078
|
+
widens: isNonEmptyList
|
|
70079
|
+
},
|
|
70080
|
+
{
|
|
70081
|
+
path: ["egress_unlisted"],
|
|
70082
|
+
reason: "lets sandboxed Bash reach hosts that allowed_domains does not name without asking first",
|
|
70083
|
+
widens: (value) => typeof value !== "string" || !["", "ask"].includes(value.trim().toLowerCase())
|
|
70084
|
+
},
|
|
70085
|
+
{
|
|
70086
|
+
path: ["host_authorities"],
|
|
70087
|
+
reason: "lets sandboxed Bash call host services beyond the SSH agent, such as a container daemon socket, which amounts to host-wide execution",
|
|
70088
|
+
widens: (value) => !Array.isArray(value) || value.some((entry) => typeof entry !== "string" || entry.trim() !== "ssh_agent")
|
|
68447
70089
|
}
|
|
68448
70090
|
];
|
|
68449
70091
|
/**
|
|
70092
|
+
* `[sandbox]` keys Reasonix reads only from the user config. In project scope
|
|
70093
|
+
* `narrowSandbox` replaces each of them with the user's value (and reports a
|
|
70094
|
+
* project `egress_unlisted` as user-only), so a repository cannot hand itself a
|
|
70095
|
+
* device, a host service or unprompted egress. A project-scope generate skips
|
|
70096
|
+
* them with a warning, the way `applyPermissionsMode` treats `mode`.
|
|
70097
|
+
*
|
|
70098
|
+
* @see https://github.com/esengine/DeepSeek-Reasonix/blob/studio-v2.44.0/internal/contract/config/project_scope.go
|
|
70099
|
+
* @see https://github.com/esengine/DeepSeek-Reasonix/blob/studio-v2.44.0/internal/contract/config/config.go
|
|
70100
|
+
*/
|
|
70101
|
+
const REASONIX_USER_ONLY_SANDBOX_KEYS = [
|
|
70102
|
+
"allow_devices",
|
|
70103
|
+
"egress_unlisted",
|
|
70104
|
+
"host_authorities"
|
|
70105
|
+
];
|
|
70106
|
+
/**
|
|
70107
|
+
* Removes the user-only `[sandbox]` keys from a project-scope override, warning
|
|
70108
|
+
* once about what was skipped. The keys are left alone in the file, so whatever
|
|
70109
|
+
* it already had survives the shallow merge. Global scope keeps them all, and
|
|
70110
|
+
* a non-table override is returned as is, so the trust check still reports its
|
|
70111
|
+
* shape rather than seeing nothing.
|
|
70112
|
+
*/
|
|
70113
|
+
function dropUserOnlySandboxKeys({ sandbox, global, relativeFilePath, logger }) {
|
|
70114
|
+
if (global || !isRecord$1(sandbox)) return sandbox;
|
|
70115
|
+
const skipped = REASONIX_USER_ONLY_SANDBOX_KEYS.filter((key) => sandbox[key] !== void 0);
|
|
70116
|
+
if (skipped.length === 0) return sandbox;
|
|
70117
|
+
logger?.warn(`Reasonix permissions: skipping ${skipped.map((key) => `reasonix.sandbox.${key}`).join(", ")} for the project-scoped ${relativeFilePath}; Reasonix reads ${skipped.length === 1 ? "this key" : "these keys"} only from the user config and replaces a project value with the user's, so generate it with --global instead.`);
|
|
70118
|
+
return Object.fromEntries(Object.entries(sandbox).filter(([key]) => !skipped.includes(key)));
|
|
70119
|
+
}
|
|
70120
|
+
/**
|
|
68450
70121
|
* Whether a `workspace_root` points somewhere other than inside the project the
|
|
68451
70122
|
* generate runs in. The key moves the write confinement rather than adding to
|
|
68452
70123
|
* it, so the ordinary value — the project directory, spelled relatively — would
|
|
@@ -68683,14 +70354,20 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
|
|
|
68683
70354
|
}));
|
|
68684
70355
|
const patch = { permissions: mergedPermissions };
|
|
68685
70356
|
if (override?.sandbox !== void 0) {
|
|
70357
|
+
const authoredSandbox = dropUserOnlySandboxKeys({
|
|
70358
|
+
sandbox: override.sandbox,
|
|
70359
|
+
global,
|
|
70360
|
+
relativeFilePath: relativeFilePathForLog,
|
|
70361
|
+
logger
|
|
70362
|
+
});
|
|
68686
70363
|
const mergedSandbox = {
|
|
68687
70364
|
...asReasonixRecord(parsed.sandbox),
|
|
68688
|
-
...asReasonixRecord(
|
|
70365
|
+
...asReasonixRecord(authoredSandbox)
|
|
68689
70366
|
};
|
|
68690
70367
|
patch.sandbox = mergedSandbox;
|
|
68691
70368
|
trustAffecting.push(...collectSandboxOverlayEntries({
|
|
68692
70369
|
existing: parsed.sandbox,
|
|
68693
|
-
authored:
|
|
70370
|
+
authored: authoredSandbox,
|
|
68694
70371
|
merged: mergedSandbox
|
|
68695
70372
|
}));
|
|
68696
70373
|
}
|
|
@@ -68743,7 +70420,7 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
|
|
|
68743
70420
|
ask: askSplit.translated,
|
|
68744
70421
|
deny: denySplit.translated
|
|
68745
70422
|
});
|
|
68746
|
-
const sandbox = asReasonixRecord(this.toml.sandbox);
|
|
70423
|
+
const sandbox = Object.fromEntries(Object.entries(asReasonixRecord(this.toml.sandbox)).filter(([key]) => this.global || !REASONIX_USER_ONLY_SANDBOX_KEYS.includes(key)));
|
|
68747
70424
|
const agentPlanMode = pickReasonixKeys(this.toml.agent, [...REASONIX_OVERRIDE_AGENT_KEYS, ...REASONIX_RETIRED_AGENT_KEYS]);
|
|
68748
70425
|
const reasonixOverride = {};
|
|
68749
70426
|
const allowDynamicBash = permissions[REASONIX_ALLOW_DYNAMIC_BASH_KEY];
|
|
@@ -69049,7 +70726,7 @@ var RooPermissions = class extends ToolPermissions {
|
|
|
69049
70726
|
};
|
|
69050
70727
|
//#endregion
|
|
69051
70728
|
//#region src/features/permissions/rovodev-permissions.ts
|
|
69052
|
-
const CATCH_ALL_PATTERN$
|
|
70729
|
+
const CATCH_ALL_PATTERN$2 = "*";
|
|
69053
70730
|
const CATEGORY_TO_TOOL_KEYS = {
|
|
69054
70731
|
read: [
|
|
69055
70732
|
"open_files",
|
|
@@ -69380,7 +71057,7 @@ function convertRulesyncToRovodevToolPermissions({ config, logger }) {
|
|
|
69380
71057
|
logger
|
|
69381
71058
|
});
|
|
69382
71059
|
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
69383
|
-
if (category === CATCH_ALL_PATTERN$
|
|
71060
|
+
if (category === CATCH_ALL_PATTERN$2) {
|
|
69384
71061
|
const toolWideDefault = convertAllToolsRules({
|
|
69385
71062
|
rules,
|
|
69386
71063
|
logger
|
|
@@ -69399,7 +71076,7 @@ function convertRulesyncToRovodevToolPermissions({ config, logger }) {
|
|
|
69399
71076
|
continue;
|
|
69400
71077
|
}
|
|
69401
71078
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
69402
|
-
if (pattern === CATCH_ALL_PATTERN$
|
|
71079
|
+
if (pattern === CATCH_ALL_PATTERN$2) {
|
|
69403
71080
|
toolPermissions.tools ??= {};
|
|
69404
71081
|
for (const toolKey of toolKeys) toolPermissions.tools[toolKey] = strictestAction(toolPermissions.tools[toolKey], action);
|
|
69405
71082
|
continue;
|
|
@@ -69422,8 +71099,8 @@ function convertRulesyncToRovodevToolPermissions({ config, logger }) {
|
|
|
69422
71099
|
* resolution never grants more than the author asked for.
|
|
69423
71100
|
*/
|
|
69424
71101
|
function warnOnEditWriteConflict({ config, logger }) {
|
|
69425
|
-
const editCatchAll = config.permission.edit?.[CATCH_ALL_PATTERN$
|
|
69426
|
-
const writeCatchAll = config.permission.write?.[CATCH_ALL_PATTERN$
|
|
71102
|
+
const editCatchAll = config.permission.edit?.[CATCH_ALL_PATTERN$2];
|
|
71103
|
+
const writeCatchAll = config.permission.write?.[CATCH_ALL_PATTERN$2];
|
|
69427
71104
|
if (editCatchAll && writeCatchAll && editCatchAll !== writeCatchAll) logger?.warn(`Rovo Dev maps both "edit" and "write" onto the same file-mutation tools, but they have conflicting catch-all permissions ("edit": "${editCatchAll}", "write": "${writeCatchAll}"). The stricter of the two ("${strictestAction(editCatchAll, writeCatchAll)}") is used.`);
|
|
69428
71105
|
}
|
|
69429
71106
|
/**
|
|
@@ -69436,7 +71113,7 @@ function warnOnEditWriteConflict({ config, logger }) {
|
|
|
69436
71113
|
function convertAllToolsRules({ rules, logger }) {
|
|
69437
71114
|
let toolWideDefault;
|
|
69438
71115
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
69439
|
-
if (pattern === CATCH_ALL_PATTERN$
|
|
71116
|
+
if (pattern === CATCH_ALL_PATTERN$2) {
|
|
69440
71117
|
toolWideDefault = action;
|
|
69441
71118
|
continue;
|
|
69442
71119
|
}
|
|
@@ -69448,7 +71125,7 @@ function convertBashRules(rules) {
|
|
|
69448
71125
|
const bash = {};
|
|
69449
71126
|
const commands = [];
|
|
69450
71127
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
69451
|
-
if (pattern === CATCH_ALL_PATTERN$
|
|
71128
|
+
if (pattern === CATCH_ALL_PATTERN$2) {
|
|
69452
71129
|
bash.default = action;
|
|
69453
71130
|
continue;
|
|
69454
71131
|
}
|
|
@@ -69465,26 +71142,26 @@ function convertBashRules(rules) {
|
|
|
69465
71142
|
*/
|
|
69466
71143
|
function convertRovodevToolPermissionsToRulesync(toolPermissions) {
|
|
69467
71144
|
const permission = {};
|
|
69468
|
-
if (isPermissionAction(toolPermissions.default)) permission[CATCH_ALL_PATTERN$
|
|
71145
|
+
if (isPermissionAction$1(toolPermissions.default)) permission[CATCH_ALL_PATTERN$2] = { [CATCH_ALL_PATTERN$2]: toolPermissions.default };
|
|
69469
71146
|
const bash = toolPermissions.bash;
|
|
69470
71147
|
if (isRecord$1(bash)) {
|
|
69471
71148
|
const bashRules = {};
|
|
69472
|
-
if (isPermissionAction(bash.default)) bashRules[CATCH_ALL_PATTERN$
|
|
71149
|
+
if (isPermissionAction$1(bash.default)) bashRules[CATCH_ALL_PATTERN$2] = bash.default;
|
|
69473
71150
|
if (Array.isArray(bash.commands)) {
|
|
69474
|
-
for (const entry of bash.commands) if (isRecord$1(entry) && typeof entry.command === "string" && isPermissionAction(entry.permission)) bashRules[entry.command] = entry.permission;
|
|
71151
|
+
for (const entry of bash.commands) if (isRecord$1(entry) && typeof entry.command === "string" && isPermissionAction$1(entry.permission)) bashRules[entry.command] = entry.permission;
|
|
69475
71152
|
}
|
|
69476
71153
|
if (Object.keys(bashRules).length > 0) permission.bash = bashRules;
|
|
69477
71154
|
}
|
|
69478
71155
|
const nestedTools = isRecord$1(toolPermissions.tools) ? toolPermissions.tools : {};
|
|
69479
|
-
const implicitLevel = isPermissionAction(toolPermissions.default) ? toolPermissions.default : "ask";
|
|
71156
|
+
const implicitLevel = isPermissionAction$1(toolPermissions.default) ? toolPermissions.default : "ask";
|
|
69480
71157
|
for (const category of new Set(Object.values(TOOL_KEY_TO_CATEGORY))) {
|
|
69481
71158
|
const levels = Object.entries(TOOL_KEY_TO_CATEGORY).filter(([, mapped]) => mapped === category).map(([toolKey]) => {
|
|
69482
71159
|
const value = Object.hasOwn(nestedTools, toolKey) ? nestedTools[toolKey] : toolPermissions[toolKey];
|
|
69483
|
-
return isPermissionAction(value) ? value : void 0;
|
|
71160
|
+
return isPermissionAction$1(value) ? value : void 0;
|
|
69484
71161
|
});
|
|
69485
71162
|
if (levels.every((level) => level === void 0)) continue;
|
|
69486
71163
|
permission[category] ??= {};
|
|
69487
|
-
permission[category][CATCH_ALL_PATTERN$
|
|
71164
|
+
permission[category][CATCH_ALL_PATTERN$2] = levels.reduce((strictest, level) => strictestAction(strictest, level ?? implicitLevel), permission[category][CATCH_ALL_PATTERN$2]);
|
|
69488
71165
|
}
|
|
69489
71166
|
if (isStringArray$1(toolPermissions.allowedExternalPaths)) for (const path of toolPermissions.allowedExternalPaths) {
|
|
69490
71167
|
permission.read ??= {};
|
|
@@ -69501,7 +71178,7 @@ function strictestAction(current, candidate) {
|
|
|
69501
71178
|
if (current === void 0) return candidate;
|
|
69502
71179
|
return ACTION_STRICTNESS.indexOf(current) <= ACTION_STRICTNESS.indexOf(candidate) ? current : candidate;
|
|
69503
71180
|
}
|
|
69504
|
-
function isPermissionAction(value) {
|
|
71181
|
+
function isPermissionAction$1(value) {
|
|
69505
71182
|
return PermissionActionSchema.safeParse(value).success;
|
|
69506
71183
|
}
|
|
69507
71184
|
//#endregion
|
|
@@ -70208,7 +71885,7 @@ const TAKT_SECURITY_POLICIES = {
|
|
|
70208
71885
|
const TAKT_SECURITY_POLICY_KEYS = Object.keys(TAKT_SECURITY_POLICIES);
|
|
70209
71886
|
const TAKT_DEFAULT_PROVIDER = "claude-sdk";
|
|
70210
71887
|
const TAKT_LEGACY_DEFAULT_PROVIDER = "claude";
|
|
70211
|
-
const CATCH_ALL_PATTERN = "*";
|
|
71888
|
+
const CATCH_ALL_PATTERN$1 = "*";
|
|
70212
71889
|
/**
|
|
70213
71890
|
* Permissions adapter for Takt (`.takt/config.yaml`).
|
|
70214
71891
|
*
|
|
@@ -70829,9 +72506,9 @@ function deriveTaktPermissionMode(config) {
|
|
|
70829
72506
|
*/
|
|
70830
72507
|
function taktModeToRulesyncConfig(mode) {
|
|
70831
72508
|
switch (mode) {
|
|
70832
|
-
case "full": return { permission: { bash: { [CATCH_ALL_PATTERN]: "allow" } } };
|
|
70833
|
-
case "edit": return { permission: { edit: { [CATCH_ALL_PATTERN]: "allow" } } };
|
|
70834
|
-
default: return { permission: { bash: { [CATCH_ALL_PATTERN]: "deny" } } };
|
|
72509
|
+
case "full": return { permission: { bash: { [CATCH_ALL_PATTERN$1]: "allow" } } };
|
|
72510
|
+
case "edit": return { permission: { edit: { [CATCH_ALL_PATTERN$1]: "allow" } } };
|
|
72511
|
+
default: return { permission: { bash: { [CATCH_ALL_PATTERN$1]: "deny" } } };
|
|
70835
72512
|
}
|
|
70836
72513
|
}
|
|
70837
72514
|
/**
|
|
@@ -70864,6 +72541,409 @@ function pickSecurityPolicies(source, report) {
|
|
|
70864
72541
|
return picked;
|
|
70865
72542
|
}
|
|
70866
72543
|
//#endregion
|
|
72544
|
+
//#region src/features/permissions/trae-permissions.ts
|
|
72545
|
+
const CUSTOM_PROFILES_KEY = "customProfiles";
|
|
72546
|
+
const DEFAULT_CUSTOM_PROFILE_KEY = "defaultCustomProfile";
|
|
72547
|
+
const APPROVAL_KEY = "approval";
|
|
72548
|
+
const COMMAND_RULES_KEY = "commandRules";
|
|
72549
|
+
const MCP_RULES_KEY = "mcpRules";
|
|
72550
|
+
const RESOURCE_AUTHORIZATION_KEY = "resourceAuthorization";
|
|
72551
|
+
const NETWORK_KEY = "network";
|
|
72552
|
+
const BASH_CATEGORY = "bash";
|
|
72553
|
+
const WEBFETCH_CATEGORY = "webfetch";
|
|
72554
|
+
const MCP_TOOL_PREFIX = "mcp__";
|
|
72555
|
+
const MCP_SEPARATOR = "__";
|
|
72556
|
+
const CATCH_ALL_PATTERN = "*";
|
|
72557
|
+
const REGEX_PREFIX = "r/";
|
|
72558
|
+
const REGEX_SUFFIX = "/";
|
|
72559
|
+
const ACTION_RANK = {
|
|
72560
|
+
allow: 0,
|
|
72561
|
+
ask: 1,
|
|
72562
|
+
deny: 2
|
|
72563
|
+
};
|
|
72564
|
+
const NETWORK_ACTIONS = ["allow", "deny"];
|
|
72565
|
+
function isPermissionAction(value) {
|
|
72566
|
+
return value === "allow" || value === "ask" || value === "deny";
|
|
72567
|
+
}
|
|
72568
|
+
function toStringList(value) {
|
|
72569
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
72570
|
+
}
|
|
72571
|
+
function escapeRegex(text) {
|
|
72572
|
+
return text.replace(/[\\^$.|?*+()[\]{}/]/g, "\\$&");
|
|
72573
|
+
}
|
|
72574
|
+
/** Build the anchored TRAE regex rule (`r/^...$/`) a canonical glob stands for. */
|
|
72575
|
+
function globToTraeRegex(pattern) {
|
|
72576
|
+
const body = pattern.split(CATCH_ALL_PATTERN).map(escapeRegex).join(".*");
|
|
72577
|
+
return `${REGEX_PREFIX}^${body}$${REGEX_SUFFIX}`;
|
|
72578
|
+
}
|
|
72579
|
+
/**
|
|
72580
|
+
* Translate a canonical `bash` glob into a TRAE `commandRules` key. TRAE
|
|
72581
|
+
* matches a key exactly (`git status`), as a prefix when it ends in `*`
|
|
72582
|
+
* (`git add *`), or as a regex when it is written `r/.../`. A glob whose only
|
|
72583
|
+
* `*` is the trailing one is already a TRAE prefix rule and a glob without
|
|
72584
|
+
* `*` an exact one; every other glob (a bare `*`, an inner `*`, or a literal
|
|
72585
|
+
* that would read as `r/.../`) becomes an anchored regex.
|
|
72586
|
+
*/
|
|
72587
|
+
function toTraeCommandKey(pattern) {
|
|
72588
|
+
const starCount = pattern.split(CATCH_ALL_PATTERN).length - 1;
|
|
72589
|
+
const looksLikeRegex = pattern.startsWith(REGEX_PREFIX) && pattern.endsWith(REGEX_SUFFIX);
|
|
72590
|
+
if (!looksLikeRegex && starCount === 0) return pattern;
|
|
72591
|
+
if (!looksLikeRegex && starCount === 1 && pattern.length > 1 && pattern.endsWith("*")) return pattern;
|
|
72592
|
+
return globToTraeRegex(pattern);
|
|
72593
|
+
}
|
|
72594
|
+
/**
|
|
72595
|
+
* Read a `commandRules` key back into a canonical `bash` glob, or `null` for a
|
|
72596
|
+
* key with no glob spelling: a regex rulesync did not write, or an exact key
|
|
72597
|
+
* with an inner `*` (TRAE matches that `*` literally).
|
|
72598
|
+
*/
|
|
72599
|
+
function fromTraeCommandKey(key) {
|
|
72600
|
+
if (!(key.startsWith(REGEX_PREFIX) && key.endsWith(REGEX_SUFFIX) && key.length > 2)) return toTraeCommandKey(key) === key ? key : null;
|
|
72601
|
+
const anchored = key.slice(2, -1).match(/^\^([\s\S]*)\$$/);
|
|
72602
|
+
if (!anchored) return null;
|
|
72603
|
+
const glob = (anchored[1] ?? "").split(".*").map((part) => part.replace(/\\(.)/g, "$1")).join(CATCH_ALL_PATTERN);
|
|
72604
|
+
return globToTraeRegex(glob) === key ? glob : null;
|
|
72605
|
+
}
|
|
72606
|
+
/**
|
|
72607
|
+
* Translate a canonical MCP category (`mcp__<server>__<tool>`,
|
|
72608
|
+
* `mcp__<server>__*` or `mcp__<server>`) into a TRAE `mcpRules` key
|
|
72609
|
+
* (`<server>__<tool>`, `<server>__*`, `<server>`). `null` when TRAE has no
|
|
72610
|
+
* spelling for it (`mcp__*`, which names no server).
|
|
72611
|
+
*/
|
|
72612
|
+
function toTraeMcpKey(category) {
|
|
72613
|
+
const rest = category.slice(5);
|
|
72614
|
+
const server = rest.split(MCP_SEPARATOR)[0] ?? "";
|
|
72615
|
+
if (server === "" || server.includes(CATCH_ALL_PATTERN)) return null;
|
|
72616
|
+
return rest;
|
|
72617
|
+
}
|
|
72618
|
+
/** Read a TRAE `mcpRules` key back into its canonical MCP category. */
|
|
72619
|
+
function fromTraeMcpKey(key) {
|
|
72620
|
+
if (key === "" || key.startsWith(MCP_SEPARATOR)) return null;
|
|
72621
|
+
return key.includes(MCP_SEPARATOR) ? `${MCP_TOOL_PREFIX}${key}` : `${MCP_TOOL_PREFIX}${key}${MCP_SEPARATOR}${CATCH_ALL_PATTERN}`;
|
|
72622
|
+
}
|
|
72623
|
+
function readRecord(source, path) {
|
|
72624
|
+
let current = source;
|
|
72625
|
+
for (const key of path) {
|
|
72626
|
+
if (!isRecord$1(current)) return {};
|
|
72627
|
+
current = current[key];
|
|
72628
|
+
}
|
|
72629
|
+
return isRecord$1(current) ? current : {};
|
|
72630
|
+
}
|
|
72631
|
+
/**
|
|
72632
|
+
* Return a copy of `root` with `value` at `path` (or the leaf removed when
|
|
72633
|
+
* `value` is `undefined`), creating the parent objects only when setting.
|
|
72634
|
+
*/
|
|
72635
|
+
function withPath(root, path, value) {
|
|
72636
|
+
const [head, ...rest] = path;
|
|
72637
|
+
if (head === void 0) return root;
|
|
72638
|
+
const next = { ...root };
|
|
72639
|
+
if (rest.length === 0) {
|
|
72640
|
+
if (value === void 0) delete next[head];
|
|
72641
|
+
else next[head] = value;
|
|
72642
|
+
return next;
|
|
72643
|
+
}
|
|
72644
|
+
const child = next[head];
|
|
72645
|
+
if (!isRecord$1(child)) {
|
|
72646
|
+
if (value === void 0) return next;
|
|
72647
|
+
next[head] = withPath({}, rest, value);
|
|
72648
|
+
return next;
|
|
72649
|
+
}
|
|
72650
|
+
next[head] = withPath(child, rest, value);
|
|
72651
|
+
return next;
|
|
72652
|
+
}
|
|
72653
|
+
const COMMAND_RULES_PATH = [
|
|
72654
|
+
DEFAULT_CUSTOM_PROFILE_KEY,
|
|
72655
|
+
APPROVAL_KEY,
|
|
72656
|
+
COMMAND_RULES_KEY
|
|
72657
|
+
];
|
|
72658
|
+
const MCP_RULES_PATH = [
|
|
72659
|
+
DEFAULT_CUSTOM_PROFILE_KEY,
|
|
72660
|
+
APPROVAL_KEY,
|
|
72661
|
+
MCP_RULES_KEY
|
|
72662
|
+
];
|
|
72663
|
+
function setRanked(ranked, key, action) {
|
|
72664
|
+
const existing = ranked.get(key);
|
|
72665
|
+
if (existing === void 0 || ACTION_RANK[action] > ACTION_RANK[existing]) ranked.set(key, action);
|
|
72666
|
+
}
|
|
72667
|
+
/**
|
|
72668
|
+
* Rebuild `commandRules` from the canonical `bash` category. An existing
|
|
72669
|
+
* entry rulesync cannot read back (a hand-written regex) is kept, and the
|
|
72670
|
+
* `execEnv` of an entry this run rewrites is carried over.
|
|
72671
|
+
*/
|
|
72672
|
+
function buildCommandRules({ bashRules, existing }) {
|
|
72673
|
+
const ranked = /* @__PURE__ */ new Map();
|
|
72674
|
+
for (const [pattern, action] of Object.entries(bashRules)) {
|
|
72675
|
+
if (isPrototypePollutionKey(pattern)) continue;
|
|
72676
|
+
setRanked(ranked, toTraeCommandKey(pattern), action);
|
|
72677
|
+
}
|
|
72678
|
+
const result = {};
|
|
72679
|
+
for (const [key, value] of Object.entries(existing)) if (!ranked.has(key) && fromTraeCommandKey(key) === null) result[key] = value;
|
|
72680
|
+
for (const [key, action] of ranked) {
|
|
72681
|
+
const previous = existing[key];
|
|
72682
|
+
const execEnv = isRecord$1(previous) ? previous.execEnv : void 0;
|
|
72683
|
+
result[key] = {
|
|
72684
|
+
approval: action,
|
|
72685
|
+
...typeof execEnv === "string" && { execEnv }
|
|
72686
|
+
};
|
|
72687
|
+
}
|
|
72688
|
+
return result;
|
|
72689
|
+
}
|
|
72690
|
+
/**
|
|
72691
|
+
* Rebuild `mcpRules`: every MCP category of the canonical config is written,
|
|
72692
|
+
* and an existing entry whose category the config does not name is kept.
|
|
72693
|
+
*/
|
|
72694
|
+
function buildMcpRules({ permission, existing, logger }) {
|
|
72695
|
+
const ranked = /* @__PURE__ */ new Map();
|
|
72696
|
+
const managedCategories = /* @__PURE__ */ new Set();
|
|
72697
|
+
for (const [category, rules] of Object.entries(permission)) {
|
|
72698
|
+
if (!category.startsWith(MCP_TOOL_PREFIX)) continue;
|
|
72699
|
+
const key = toTraeMcpKey(category);
|
|
72700
|
+
if (key === null) {
|
|
72701
|
+
logger?.warn(`TRAE permissions: skipping the '${category}' category — TRAE's mcpRules name an MCP server ('server', 'server__*' or 'server__tool').`);
|
|
72702
|
+
continue;
|
|
72703
|
+
}
|
|
72704
|
+
managedCategories.add(category);
|
|
72705
|
+
for (const [pattern, action] of Object.entries(rules)) {
|
|
72706
|
+
if (pattern !== CATCH_ALL_PATTERN) {
|
|
72707
|
+
logger?.warn(`TRAE permissions: skipping the '${category}' rule '${pattern}' — TRAE matches MCP tools by name only.`);
|
|
72708
|
+
continue;
|
|
72709
|
+
}
|
|
72710
|
+
setRanked(ranked, key, action);
|
|
72711
|
+
}
|
|
72712
|
+
}
|
|
72713
|
+
const result = {};
|
|
72714
|
+
for (const [key, value] of Object.entries(existing)) {
|
|
72715
|
+
const category = fromTraeMcpKey(key);
|
|
72716
|
+
if (!ranked.has(key) && (category === null || !managedCategories.has(category))) result[key] = value;
|
|
72717
|
+
}
|
|
72718
|
+
for (const [key, action] of ranked) result[key] = { approval: action };
|
|
72719
|
+
return result;
|
|
72720
|
+
}
|
|
72721
|
+
/** Split the canonical `webfetch` rules into TRAE's network allow/deny lists. */
|
|
72722
|
+
function buildNetworkLists({ webfetchRules, logger }) {
|
|
72723
|
+
const ranked = /* @__PURE__ */ new Map();
|
|
72724
|
+
for (const [pattern, action] of Object.entries(webfetchRules)) {
|
|
72725
|
+
if (isPrototypePollutionKey(pattern)) continue;
|
|
72726
|
+
if (pattern === CATCH_ALL_PATTERN) {
|
|
72727
|
+
logger?.warn(`TRAE permissions: skipping the 'webfetch' rule '*' — TRAE sets the default network policy in the custom permission mode ('customProfiles.defaultCustomProfile.network.default'), not in resourceAuthorization.`);
|
|
72728
|
+
continue;
|
|
72729
|
+
}
|
|
72730
|
+
if (action === "ask") {
|
|
72731
|
+
logger?.warn(`TRAE permissions: skipping the 'webfetch' ask rule '${pattern}' — resourceAuthorization.network has only allow and deny lists.`);
|
|
72732
|
+
continue;
|
|
72733
|
+
}
|
|
72734
|
+
setRanked(ranked, pattern, action);
|
|
72735
|
+
}
|
|
72736
|
+
const lists = {
|
|
72737
|
+
allow: [],
|
|
72738
|
+
deny: []
|
|
72739
|
+
};
|
|
72740
|
+
for (const [pattern, action] of ranked) if (action === "allow" || action === "deny") lists[action].push(pattern);
|
|
72741
|
+
return lists;
|
|
72742
|
+
}
|
|
72743
|
+
function warnUnmappedCategories({ permission, logger }) {
|
|
72744
|
+
for (const category of Object.keys(permission)) if (category !== BASH_CATEGORY && category !== WEBFETCH_CATEGORY && !category.startsWith(MCP_TOOL_PREFIX)) logger?.warn(`TRAE permissions: skipping the '${category}' category — TRAE's global permission file has no rule for it (only 'bash', 'webfetch' and 'mcp__*' are mapped).`);
|
|
72745
|
+
}
|
|
72746
|
+
/** A rebuilt map, or `undefined` (remove the key) when it ended up empty. */
|
|
72747
|
+
function nonEmpty(value) {
|
|
72748
|
+
return Object.keys(value).length > 0 ? value : void 0;
|
|
72749
|
+
}
|
|
72750
|
+
/**
|
|
72751
|
+
* The new `customProfiles` value: the existing one with `commandRules` and
|
|
72752
|
+
* `mcpRules` rebuilt for the categories the config names. The existing value
|
|
72753
|
+
* is returned untouched when the config names none of them.
|
|
72754
|
+
*/
|
|
72755
|
+
function buildCustomProfiles({ existing, permission, logger }) {
|
|
72756
|
+
const bashRules = permission[BASH_CATEGORY];
|
|
72757
|
+
const managesMcp = Object.keys(permission).some((category) => category.startsWith(MCP_TOOL_PREFIX));
|
|
72758
|
+
if (bashRules === void 0 && !managesMcp) return existing;
|
|
72759
|
+
let result = isRecord$1(existing) ? existing : {};
|
|
72760
|
+
if (bashRules !== void 0) {
|
|
72761
|
+
const commandRules = buildCommandRules({
|
|
72762
|
+
bashRules,
|
|
72763
|
+
existing: readRecord(result, COMMAND_RULES_PATH)
|
|
72764
|
+
});
|
|
72765
|
+
result = withPath(result, COMMAND_RULES_PATH, nonEmpty(commandRules));
|
|
72766
|
+
}
|
|
72767
|
+
if (managesMcp) {
|
|
72768
|
+
const mcpRules = buildMcpRules({
|
|
72769
|
+
permission,
|
|
72770
|
+
existing: readRecord(result, MCP_RULES_PATH),
|
|
72771
|
+
logger
|
|
72772
|
+
});
|
|
72773
|
+
result = withPath(result, MCP_RULES_PATH, nonEmpty(mcpRules));
|
|
72774
|
+
}
|
|
72775
|
+
return existing === void 0 ? nonEmpty(result) : result;
|
|
72776
|
+
}
|
|
72777
|
+
/**
|
|
72778
|
+
* The new `resourceAuthorization` value: the existing one with
|
|
72779
|
+
* `network.allow` / `deny` rebuilt from the `webfetch` category, or the
|
|
72780
|
+
* existing value untouched when the config has no `webfetch` category.
|
|
72781
|
+
*/
|
|
72782
|
+
function buildResourceAuthorization({ existing, permission, logger }) {
|
|
72783
|
+
const webfetchRules = permission[WEBFETCH_CATEGORY];
|
|
72784
|
+
if (webfetchRules === void 0) return existing;
|
|
72785
|
+
const lists = buildNetworkLists({
|
|
72786
|
+
webfetchRules,
|
|
72787
|
+
logger
|
|
72788
|
+
});
|
|
72789
|
+
let result = isRecord$1(existing) ? existing : {};
|
|
72790
|
+
for (const action of NETWORK_ACTIONS) result = withPath(result, [NETWORK_KEY, action], nonEmpty(lists[action]));
|
|
72791
|
+
return existing === void 0 ? nonEmpty(result) : result;
|
|
72792
|
+
}
|
|
72793
|
+
/**
|
|
72794
|
+
* TRAE permissions (global scope only).
|
|
72795
|
+
*
|
|
72796
|
+
* TRAE keeps its global permission policy and its custom permission mode in
|
|
72797
|
+
* one user file, `~/.trae/permission/global.json`. Rulesync writes three parts
|
|
72798
|
+
* of it and leaves the rest (`shellSandbox`, `approval.reviewer`,
|
|
72799
|
+
* `sceneRules`, the default `filesystem` / `network` policy,
|
|
72800
|
+
* `resourceAuthorization.filesystem`) as it is:
|
|
72801
|
+
*
|
|
72802
|
+
* - `bash` → `customProfiles.defaultCustomProfile.approval.commandRules`,
|
|
72803
|
+
* `{ "<pattern>": { "approval": "allow" | "ask" | "deny" } }`. A pattern is
|
|
72804
|
+
* matched exactly, as a prefix when it ends in `*`, or as a regex written
|
|
72805
|
+
* `r/.../`; other globs become anchored regexes.
|
|
72806
|
+
* - `mcp__<server>__<tool>` / `mcp__<server>__*` →
|
|
72807
|
+
* `customProfiles.defaultCustomProfile.approval.mcpRules`
|
|
72808
|
+
* (`<server>__<tool>` / `<server>__*`).
|
|
72809
|
+
* - `webfetch` domains → `resourceAuthorization.network.allow` / `deny`.
|
|
72810
|
+
*
|
|
72811
|
+
* The file is never deleted, and an unparseable file is an error rather than
|
|
72812
|
+
* being replaced.
|
|
72813
|
+
*
|
|
72814
|
+
* @see https://docs.trae.ai/ide/permission-and-approval?_lang=en
|
|
72815
|
+
* @see https://docs.trae.ai/ide/custom-global-permission-configuration-reference?_lang=en
|
|
72816
|
+
* @see https://docs.trae.ai/ide/custom-permission-mode-configuration-reference?_lang=en
|
|
72817
|
+
*/
|
|
72818
|
+
var TraePermissions = class TraePermissions extends ToolPermissions {
|
|
72819
|
+
constructor(params) {
|
|
72820
|
+
super({
|
|
72821
|
+
...params,
|
|
72822
|
+
fileContent: params.fileContent ?? "{}"
|
|
72823
|
+
});
|
|
72824
|
+
}
|
|
72825
|
+
isDeletable() {
|
|
72826
|
+
return false;
|
|
72827
|
+
}
|
|
72828
|
+
static getSettablePaths(_options = {}) {
|
|
72829
|
+
return {
|
|
72830
|
+
relativeDirPath: TRAE_PERMISSION_DIR_PATH,
|
|
72831
|
+
relativeFilePath: TRAE_PERMISSION_FILE_NAME
|
|
72832
|
+
};
|
|
72833
|
+
}
|
|
72834
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = true }) {
|
|
72835
|
+
const paths = TraePermissions.getSettablePaths({ global });
|
|
72836
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
|
|
72837
|
+
return new TraePermissions({
|
|
72838
|
+
outputRoot,
|
|
72839
|
+
relativeDirPath: paths.relativeDirPath,
|
|
72840
|
+
relativeFilePath: paths.relativeFilePath,
|
|
72841
|
+
fileContent,
|
|
72842
|
+
validate,
|
|
72843
|
+
global
|
|
72844
|
+
});
|
|
72845
|
+
}
|
|
72846
|
+
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = true, logger }) {
|
|
72847
|
+
const paths = TraePermissions.getSettablePaths({ global });
|
|
72848
|
+
const relativePath = (0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath);
|
|
72849
|
+
const filePath = (0, node_path.join)(outputRoot, relativePath);
|
|
72850
|
+
const existingContent = await readFileContentOrNull(filePath) ?? "{}";
|
|
72851
|
+
const existing = parseTraePermissionConfig({
|
|
72852
|
+
fileContent: existingContent,
|
|
72853
|
+
relativePath
|
|
72854
|
+
});
|
|
72855
|
+
const permission = honorAllToolsOnBash(rulesyncPermissions.getJson().permission);
|
|
72856
|
+
warnUnmappedCategories({
|
|
72857
|
+
permission,
|
|
72858
|
+
logger
|
|
72859
|
+
});
|
|
72860
|
+
return new TraePermissions({
|
|
72861
|
+
outputRoot,
|
|
72862
|
+
relativeDirPath: paths.relativeDirPath,
|
|
72863
|
+
relativeFilePath: paths.relativeFilePath,
|
|
72864
|
+
fileContent: applySharedConfigPatch({
|
|
72865
|
+
fileKey: sharedConfigFileKey(paths),
|
|
72866
|
+
feature: "permissions",
|
|
72867
|
+
existingContent,
|
|
72868
|
+
patch: {
|
|
72869
|
+
[CUSTOM_PROFILES_KEY]: buildCustomProfiles({
|
|
72870
|
+
existing: existing[CUSTOM_PROFILES_KEY],
|
|
72871
|
+
permission,
|
|
72872
|
+
logger
|
|
72873
|
+
}),
|
|
72874
|
+
[RESOURCE_AUTHORIZATION_KEY]: buildResourceAuthorization({
|
|
72875
|
+
existing: existing[RESOURCE_AUTHORIZATION_KEY],
|
|
72876
|
+
permission,
|
|
72877
|
+
logger
|
|
72878
|
+
})
|
|
72879
|
+
},
|
|
72880
|
+
filePath
|
|
72881
|
+
}),
|
|
72882
|
+
validate: true,
|
|
72883
|
+
global
|
|
72884
|
+
});
|
|
72885
|
+
}
|
|
72886
|
+
toRulesyncPermissions() {
|
|
72887
|
+
const config = parseTraePermissionConfig({
|
|
72888
|
+
fileContent: this.getFileContent() || "{}",
|
|
72889
|
+
relativePath: (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())
|
|
72890
|
+
});
|
|
72891
|
+
const customProfiles = readRecord(config, [CUSTOM_PROFILES_KEY]);
|
|
72892
|
+
const permission = {};
|
|
72893
|
+
const put = (category, pattern, action) => {
|
|
72894
|
+
if (isPrototypePollutionKey(category) || isPrototypePollutionKey(pattern)) return;
|
|
72895
|
+
const rules = permission[category] ??= {};
|
|
72896
|
+
const existing = rules[pattern];
|
|
72897
|
+
if (existing === void 0 || ACTION_RANK[action] > ACTION_RANK[existing]) rules[pattern] = action;
|
|
72898
|
+
};
|
|
72899
|
+
for (const [key, value] of Object.entries(readRecord(customProfiles, COMMAND_RULES_PATH))) {
|
|
72900
|
+
const pattern = fromTraeCommandKey(key);
|
|
72901
|
+
const approval = isRecord$1(value) ? value.approval : void 0;
|
|
72902
|
+
if (pattern !== null && pattern.trim() !== "" && isPermissionAction(approval)) put(BASH_CATEGORY, pattern, approval);
|
|
72903
|
+
}
|
|
72904
|
+
for (const [key, value] of Object.entries(readRecord(customProfiles, MCP_RULES_PATH))) {
|
|
72905
|
+
const category = fromTraeMcpKey(key);
|
|
72906
|
+
const approval = isRecord$1(value) ? value.approval : void 0;
|
|
72907
|
+
if (category !== null && isPermissionAction(approval)) put(category, CATCH_ALL_PATTERN, approval);
|
|
72908
|
+
}
|
|
72909
|
+
const network = readRecord(config, [RESOURCE_AUTHORIZATION_KEY, NETWORK_KEY]);
|
|
72910
|
+
for (const action of NETWORK_ACTIONS) for (const pattern of toStringList(network[action])) if (pattern.trim() !== "") put(WEBFETCH_CATEGORY, pattern, action);
|
|
72911
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
|
|
72912
|
+
}
|
|
72913
|
+
validate() {
|
|
72914
|
+
return {
|
|
72915
|
+
success: true,
|
|
72916
|
+
error: null
|
|
72917
|
+
};
|
|
72918
|
+
}
|
|
72919
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = true }) {
|
|
72920
|
+
return new TraePermissions({
|
|
72921
|
+
outputRoot,
|
|
72922
|
+
relativeDirPath,
|
|
72923
|
+
relativeFilePath,
|
|
72924
|
+
fileContent: "{}",
|
|
72925
|
+
validate: false,
|
|
72926
|
+
global
|
|
72927
|
+
});
|
|
72928
|
+
}
|
|
72929
|
+
};
|
|
72930
|
+
/**
|
|
72931
|
+
* Fail closed on a syntax error or non-object root, so a broken file is
|
|
72932
|
+
* surfaced rather than partially imported or overwritten.
|
|
72933
|
+
*/
|
|
72934
|
+
function parseTraePermissionConfig({ fileContent, relativePath }) {
|
|
72935
|
+
try {
|
|
72936
|
+
return parseSharedConfig({
|
|
72937
|
+
format: "json",
|
|
72938
|
+
fileContent,
|
|
72939
|
+
filePath: relativePath,
|
|
72940
|
+
invalidRootPolicy: "error"
|
|
72941
|
+
});
|
|
72942
|
+
} catch (error) {
|
|
72943
|
+
throw new Error(`Failed to parse TRAE permission config in ${relativePath}: ${formatError(error)}`, { cause: error });
|
|
72944
|
+
}
|
|
72945
|
+
}
|
|
72946
|
+
//#endregion
|
|
70867
72947
|
//#region src/utils/display-width.ts
|
|
70868
72948
|
/**
|
|
70869
72949
|
* Characters a terminal draws two columns wide: the East Asian Wide and
|
|
@@ -74149,6 +76229,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
|
|
|
74149
76229
|
supportsImport: true
|
|
74150
76230
|
}
|
|
74151
76231
|
}],
|
|
76232
|
+
["trae", {
|
|
76233
|
+
class: TraePermissions,
|
|
76234
|
+
meta: {
|
|
76235
|
+
supportsProject: false,
|
|
76236
|
+
supportsGlobal: true,
|
|
76237
|
+
supportsImport: true
|
|
76238
|
+
}
|
|
76239
|
+
}],
|
|
74152
76240
|
["zoocode", {
|
|
74153
76241
|
class: ZoocodePermissions,
|
|
74154
76242
|
meta: {
|
|
@@ -74253,7 +76341,7 @@ var PermissionsProcessor = class extends FeatureProcessor {
|
|
|
74253
76341
|
})];
|
|
74254
76342
|
}
|
|
74255
76343
|
async convertToolFilesToRulesyncFiles(toolFiles) {
|
|
74256
|
-
return toolFiles.filter((f) => f instanceof ToolPermissions).map((p) => p.toRulesyncPermissions());
|
|
76344
|
+
return toolFiles.filter((f) => f instanceof ToolPermissions).map((p) => p.toRulesyncPermissions({ logger: this.logger }));
|
|
74257
76345
|
}
|
|
74258
76346
|
static getToolTargets({ global = false, importOnly = false } = {}) {
|
|
74259
76347
|
return [...toolPermissionsFactories.entries()].filter(([, f]) => global ? f.meta.supportsGlobal : f.meta.supportsProject).filter(([, f]) => importOnly ? f.meta.supportsImport : true).map(([target]) => target);
|
|
@@ -79166,8 +81254,13 @@ var FactorydroidSkill = class FactorydroidSkill extends ToolSkill {
|
|
|
79166
81254
|
* slash command), so this target reuses {@link AgentsSkillsSkill} with GitLab's
|
|
79167
81255
|
* locations:
|
|
79168
81256
|
*
|
|
79169
|
-
* - Project skills
|
|
79170
|
-
* root, which both the GitLab UI flows and the GitLab Duo CLI read.
|
|
81257
|
+
* - Project skills are written to the cross-tool `.agents/skills/<name>/SKILL.md`
|
|
81258
|
+
* root, which both the GitLab UI flows and the GitLab Duo CLI read. GitLab
|
|
81259
|
+
* Duo also reads a non-hidden `skills/<name>/SKILL.md` at the project root
|
|
81260
|
+
* (`.agents/skills/` wins on a name clash); rulesync imports from it but
|
|
81261
|
+
* never writes or sweeps it, because that directory routinely holds
|
|
81262
|
+
* hand-authored skills. Earlier rulesync versions wrote there, so a skill
|
|
81263
|
+
* generated by one of them has to be removed from `skills/` by hand.
|
|
79171
81264
|
* - User skills live in `~/.gitlab/duo/skills/` and are loaded by the CLI only
|
|
79172
81265
|
* when started with `--enable-global-skills true` (or
|
|
79173
81266
|
* `GITLAB_ENABLE_GLOBAL_SKILLS=true`).
|
|
@@ -79176,18 +81269,20 @@ var FactorydroidSkill = class FactorydroidSkill extends ToolSkill {
|
|
|
79176
81269
|
*/
|
|
79177
81270
|
var GitlabduoSkill = class extends AgentsSkillsSkill {
|
|
79178
81271
|
static getSettablePaths({ global = false } = {}) {
|
|
79179
|
-
return { relativeDirPath:
|
|
81272
|
+
if (global) return { relativeDirPath: GITLABDUO_GLOBAL_SKILLS_DIR };
|
|
81273
|
+
return {
|
|
81274
|
+
relativeDirPath: GITLABDUO_SKILLS_DIR,
|
|
81275
|
+
importOnlySkillRoots: [GITLABDUO_LEGACY_SKILLS_DIR]
|
|
81276
|
+
};
|
|
79180
81277
|
}
|
|
79181
81278
|
static isTargetedByRulesyncSkill(rulesyncSkill) {
|
|
79182
81279
|
const targets = rulesyncSkill.getFrontmatter().targets;
|
|
79183
81280
|
return targets.includes("*") || targets.includes("gitlabduo");
|
|
79184
81281
|
}
|
|
79185
81282
|
/**
|
|
79186
|
-
* The project
|
|
79187
|
-
*
|
|
79188
|
-
*
|
|
79189
|
-
* `--delete` orphan sweep skips that root entirely through the factory's
|
|
79190
|
-
* `committedOutput` meta; this filter still applies to the global sweep.)
|
|
81283
|
+
* The import-only project-root `skills/` directory is one many repositories
|
|
81284
|
+
* already use for unrelated content, so only a subdirectory holding a
|
|
81285
|
+
* `SKILL.md` is treated as a skill, in every root this target reads.
|
|
79191
81286
|
*/
|
|
79192
81287
|
static async isDirOwned({ outputRoot, relativeDirPath, dirName }) {
|
|
79193
81288
|
return await fileExists((0, node_path.join)(outputRoot, relativeDirPath, dirName, SKILL_FILE_NAME));
|
|
@@ -80740,9 +82835,44 @@ var MimocodeSkill = class extends OpenCodeSkill {
|
|
|
80740
82835
|
//#region src/features/skills/musecode-skill.ts
|
|
80741
82836
|
const MusecodeSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
80742
82837
|
name: zod_mini.z.string(),
|
|
80743
|
-
description: zod_mini.z.string()
|
|
82838
|
+
description: zod_mini.z.string(),
|
|
82839
|
+
license: zod_mini.z.optional(zod_mini.z.string()),
|
|
82840
|
+
compatibility: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})])),
|
|
82841
|
+
metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
|
|
80744
82842
|
});
|
|
80745
82843
|
/**
|
|
82844
|
+
* Build the Muse Code frontmatter. `license`, `compatibility` and `metadata`
|
|
82845
|
+
* come from the `musecode` section with the root-level values as fallback, and
|
|
82846
|
+
* are normalized exactly as the `agentsskills` writer normalizes them: at
|
|
82847
|
+
* project scope both targets write the same `.agents/skills/<name>/SKILL.md`,
|
|
82848
|
+
* so for a skill without per-target overrides the file no longer depends on
|
|
82849
|
+
* which of the two runs last.
|
|
82850
|
+
*/
|
|
82851
|
+
function buildMusecodeFrontmatter(rulesyncFrontmatter) {
|
|
82852
|
+
const section = rulesyncFrontmatter.musecode;
|
|
82853
|
+
const license = resolveLicense({
|
|
82854
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
82855
|
+
section
|
|
82856
|
+
});
|
|
82857
|
+
const compatibility = resolveCompatibility({
|
|
82858
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
82859
|
+
section
|
|
82860
|
+
});
|
|
82861
|
+
const metadata = resolveMetadata({
|
|
82862
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
82863
|
+
section
|
|
82864
|
+
});
|
|
82865
|
+
return {
|
|
82866
|
+
name: rulesyncFrontmatter.name,
|
|
82867
|
+
description: rulesyncFrontmatter.description,
|
|
82868
|
+
...toSpecConformantAgentSkillFields({
|
|
82869
|
+
...license !== void 0 && { license },
|
|
82870
|
+
...compatibility !== void 0 && { compatibility },
|
|
82871
|
+
...metadata !== void 0 && { metadata }
|
|
82872
|
+
})
|
|
82873
|
+
};
|
|
82874
|
+
}
|
|
82875
|
+
/**
|
|
80746
82876
|
* Represents a Meta Muse Code skill directory.
|
|
80747
82877
|
*
|
|
80748
82878
|
* Muse Code reads Agent Skills as `<skill-id>/SKILL.md` directories from the
|
|
@@ -80799,10 +82929,16 @@ var MusecodeSkill = class MusecodeSkill extends ToolSkill {
|
|
|
80799
82929
|
}
|
|
80800
82930
|
toRulesyncSkill() {
|
|
80801
82931
|
const frontmatter = this.getFrontmatter();
|
|
82932
|
+
const musecodeSection = {
|
|
82933
|
+
...frontmatter.license !== void 0 && { license: frontmatter.license },
|
|
82934
|
+
...frontmatter.compatibility !== void 0 && { compatibility: frontmatter.compatibility },
|
|
82935
|
+
...frontmatter.metadata !== void 0 && { metadata: frontmatter.metadata }
|
|
82936
|
+
};
|
|
80802
82937
|
const rulesyncFrontmatter = {
|
|
80803
82938
|
name: frontmatter.name,
|
|
80804
82939
|
description: frontmatter.description,
|
|
80805
|
-
targets: ["*"]
|
|
82940
|
+
targets: ["*"],
|
|
82941
|
+
...Object.keys(musecodeSection).length > 0 && { musecode: musecodeSection }
|
|
80806
82942
|
};
|
|
80807
82943
|
return new RulesyncSkill({
|
|
80808
82944
|
outputRoot: this.outputRoot,
|
|
@@ -80817,11 +82953,7 @@ var MusecodeSkill = class MusecodeSkill extends ToolSkill {
|
|
|
80817
82953
|
}
|
|
80818
82954
|
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
|
|
80819
82955
|
const settablePaths = MusecodeSkill.getSettablePaths({ global });
|
|
80820
|
-
const
|
|
80821
|
-
const musecodeFrontmatter = {
|
|
80822
|
-
name: rulesyncFrontmatter.name,
|
|
80823
|
-
description: rulesyncFrontmatter.description
|
|
80824
|
-
};
|
|
82956
|
+
const musecodeFrontmatter = buildMusecodeFrontmatter(rulesyncSkill.getFrontmatter());
|
|
80825
82957
|
return new MusecodeSkill({
|
|
80826
82958
|
outputRoot,
|
|
80827
82959
|
relativeDirPath: settablePaths.relativeDirPath,
|
|
@@ -83272,8 +85404,7 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
|
|
|
83272
85404
|
supportsProject: true,
|
|
83273
85405
|
supportsSimulated: false,
|
|
83274
85406
|
supportsGlobal: true,
|
|
83275
|
-
lenientImport: true
|
|
83276
|
-
committedOutput: true
|
|
85407
|
+
lenientImport: true
|
|
83277
85408
|
}
|
|
83278
85409
|
}],
|
|
83279
85410
|
["grokcli", {
|
|
@@ -83886,10 +86017,6 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
83886
86017
|
}
|
|
83887
86018
|
async loadToolDirsToDelete() {
|
|
83888
86019
|
const factory = this.getFactory(this.toolTarget);
|
|
83889
|
-
if (factory.meta.committedOutput === true && !this.global) {
|
|
83890
|
-
this.logger.debug(`Skipping the orphan sweep for ${this.toolTarget} skills: the project root is committed and user-owned`);
|
|
83891
|
-
return [];
|
|
83892
|
-
}
|
|
83893
86020
|
const paths = factory.class.getSettablePaths({ global: this.global });
|
|
83894
86021
|
const roots = toolSkillSearchRoots(paths);
|
|
83895
86022
|
const toolSkills = [];
|
|
@@ -85434,6 +87561,14 @@ const PLUGIN_FORBIDDEN_FIELDS = [
|
|
|
85434
87561
|
"mcpServers",
|
|
85435
87562
|
"permissionMode"
|
|
85436
87563
|
];
|
|
87564
|
+
/**
|
|
87565
|
+
* Claude Code ignores these for plugin-shipped agents without any security
|
|
87566
|
+
* rationale. They are dropped with a separate warning so the output does not
|
|
87567
|
+
* suggest a behavior the plugin agent never gets.
|
|
87568
|
+
*
|
|
87569
|
+
* @see https://code.claude.com/docs/en/sub-agents#supported-frontmatter-fields
|
|
87570
|
+
*/
|
|
87571
|
+
const PLUGIN_IGNORED_FIELDS = ["initialPrompt"];
|
|
85437
87572
|
/** The only `isolation` value plugin agents accept. */
|
|
85438
87573
|
const PLUGIN_ISOLATION_VALUE = "worktree";
|
|
85439
87574
|
var ClaudecodePluginSubagent = class extends ClaudecodeSubagent {
|
|
@@ -85453,6 +87588,9 @@ var ClaudecodePluginSubagent = class extends ClaudecodeSubagent {
|
|
|
85453
87588
|
const dropped = PLUGIN_FORBIDDEN_FIELDS.filter((field) => sanitized[field] !== void 0);
|
|
85454
87589
|
for (const field of PLUGIN_FORBIDDEN_FIELDS) delete sanitized[field];
|
|
85455
87590
|
if (dropped.length > 0) logger?.warn(`Dropping ${dropped.join(", ")} from claudecode-plugin subagent ${relativeFilePath}: Claude Code does not support these fields for plugin-shipped agents.`);
|
|
87591
|
+
const ignored = PLUGIN_IGNORED_FIELDS.filter((field) => sanitized[field] !== void 0);
|
|
87592
|
+
for (const field of PLUGIN_IGNORED_FIELDS) delete sanitized[field];
|
|
87593
|
+
if (ignored.length > 0) logger?.warn(`Dropping ${ignored.join(", ")} from claudecode-plugin subagent ${relativeFilePath}: Claude Code ignores these fields for plugin-shipped agents.`);
|
|
85456
87594
|
if (sanitized.isolation !== void 0 && sanitized.isolation !== PLUGIN_ISOLATION_VALUE) {
|
|
85457
87595
|
logger?.warn(`Dropping isolation "${sanitized.isolation}" from claudecode-plugin subagent ${relativeFilePath}: "${PLUGIN_ISOLATION_VALUE}" is the only value Claude Code accepts for plugin-shipped agents.`);
|
|
85458
87596
|
delete sanitized.isolation;
|
|
@@ -88906,6 +91044,32 @@ async function resolveOpenCodeAgentPrompt({ prompt, configDir }) {
|
|
|
88906
91044
|
configDir
|
|
88907
91045
|
});
|
|
88908
91046
|
}
|
|
91047
|
+
/**
|
|
91048
|
+
* Resolves the `prompt` of each inline agent entry of one config file against
|
|
91049
|
+
* that file's directory, so entries from several files can be merged without
|
|
91050
|
+
* losing where a `"{file:./path}"` reference points.
|
|
91051
|
+
*/
|
|
91052
|
+
async function resolveOpenCodeAgentPrompts({ entries, configDir }) {
|
|
91053
|
+
if (entries === null) return null;
|
|
91054
|
+
const resolved = {};
|
|
91055
|
+
for (const [name, rawEntry] of Object.entries(entries)) {
|
|
91056
|
+
const entry = asOpencodeEntries(rawEntry);
|
|
91057
|
+
const value = entry === null || !Object.hasOwn(entry, "prompt") ? rawEntry : {
|
|
91058
|
+
...entry,
|
|
91059
|
+
prompt: await resolveOpenCodeAgentPrompt({
|
|
91060
|
+
prompt: entry.prompt,
|
|
91061
|
+
configDir
|
|
91062
|
+
})
|
|
91063
|
+
};
|
|
91064
|
+
Object.defineProperty(resolved, name, {
|
|
91065
|
+
value,
|
|
91066
|
+
enumerable: true,
|
|
91067
|
+
configurable: true,
|
|
91068
|
+
writable: true
|
|
91069
|
+
});
|
|
91070
|
+
}
|
|
91071
|
+
return resolved;
|
|
91072
|
+
}
|
|
88909
91073
|
var OpenCodeSubagent = class extends OpenCodeStyleSubagent {
|
|
88910
91074
|
/** Directory layout; OpenCode forks (MiMo Code) override it. */
|
|
88911
91075
|
static layout = OPENCODE_LAYOUT;
|
|
@@ -88977,8 +91141,9 @@ var OpenCodeSubagent = class extends OpenCodeStyleSubagent {
|
|
|
88977
91141
|
});
|
|
88978
91142
|
}
|
|
88979
91143
|
/**
|
|
88980
|
-
* Imports agents defined inline in `opencode.json` / `opencode.jsonc`
|
|
88981
|
-
*
|
|
91144
|
+
* Imports agents defined inline in `opencode.json` / `opencode.jsonc` (and,
|
|
91145
|
+
* at project scope, `.opencode/opencode.json(c)`, which OpenCode merges over
|
|
91146
|
+
* them) under the top-level `agent` key — or its V2 spelling `agents`, see
|
|
88982
91147
|
* `getOpencodeAgentEntries` — (in addition to the Markdown files under
|
|
88983
91148
|
* `.opencode/agents/`). Each entry's `prompt` becomes the subagent body
|
|
88984
91149
|
* (a `"{file:./path}"` reference is resolved relative to the config file's
|
|
@@ -88991,30 +91156,26 @@ var OpenCodeSubagent = class extends OpenCodeStyleSubagent {
|
|
|
88991
91156
|
* @see https://opencode.ai/docs/agents/#json
|
|
88992
91157
|
*/
|
|
88993
91158
|
static async loadAdditionalImportFiles({ outputRoot = process.cwd(), global = false } = {}) {
|
|
88994
|
-
const
|
|
88995
|
-
config: await readOpencodeConfig({
|
|
88996
|
-
outputRoot,
|
|
88997
|
-
global,
|
|
88998
|
-
layout: this.layout
|
|
88999
|
-
}),
|
|
89000
|
-
layout: this.layout
|
|
89001
|
-
});
|
|
89002
|
-
if (!agentEntries) return [];
|
|
89003
|
-
const paths = this.getSettablePaths({ global });
|
|
89004
|
-
const configDir = getOpencodeConfigDir({
|
|
91159
|
+
const layers = await readOpencodeConfigLayers({
|
|
89005
91160
|
outputRoot,
|
|
89006
91161
|
global,
|
|
89007
91162
|
layout: this.layout
|
|
89008
91163
|
});
|
|
91164
|
+
const agentEntries = mergeOpencodeEntryLayers(await Promise.all(layers.map(({ config, configDir }) => resolveOpenCodeAgentPrompts({
|
|
91165
|
+
entries: getOpencodeAgentEntries({
|
|
91166
|
+
config,
|
|
91167
|
+
layout: this.layout
|
|
91168
|
+
}),
|
|
91169
|
+
configDir
|
|
91170
|
+
}))));
|
|
91171
|
+
if (!agentEntries) return [];
|
|
91172
|
+
const paths = this.getSettablePaths({ global });
|
|
89009
91173
|
const subagents = [];
|
|
89010
91174
|
for (const [name, rawEntry] of Object.entries(agentEntries)) {
|
|
89011
91175
|
const entry = asOpencodeEntries(rawEntry);
|
|
89012
91176
|
if (!entry) continue;
|
|
89013
91177
|
const { prompt, ...frontmatterFields } = entry;
|
|
89014
|
-
const body =
|
|
89015
|
-
prompt,
|
|
89016
|
-
configDir
|
|
89017
|
-
});
|
|
91178
|
+
const body = typeof prompt === "string" ? prompt : "";
|
|
89018
91179
|
const parseResult = OpenCodeSubagentFrontmatterSchema.safeParse(frontmatterFields);
|
|
89019
91180
|
if (!parseResult.success) continue;
|
|
89020
91181
|
const frontmatter = parseResult.data;
|
|
@@ -91774,7 +93935,7 @@ var ToolRule = class extends ToolFile {
|
|
|
91774
93935
|
body: this.getFileContent()
|
|
91775
93936
|
});
|
|
91776
93937
|
}
|
|
91777
|
-
toRulesyncRuleDefault() {
|
|
93938
|
+
toRulesyncRuleDefault({ body } = {}) {
|
|
91778
93939
|
return new RulesyncRule({
|
|
91779
93940
|
outputRoot: process.cwd(),
|
|
91780
93941
|
relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
|
|
@@ -91785,7 +93946,7 @@ var ToolRule = class extends ToolFile {
|
|
|
91785
93946
|
description: this.description,
|
|
91786
93947
|
globs: this.globs ?? (this.isRoot() ? ["**/*"] : [])
|
|
91787
93948
|
},
|
|
91788
|
-
body: this.getFileContent()
|
|
93949
|
+
body: body ?? this.getFileContent()
|
|
91789
93950
|
});
|
|
91790
93951
|
}
|
|
91791
93952
|
isRoot() {
|
|
@@ -92891,11 +95052,19 @@ var AntigravityIdeRule = class extends ToolRule {
|
|
|
92891
95052
|
}
|
|
92892
95053
|
super({
|
|
92893
95054
|
...rest,
|
|
92894
|
-
fileContent: rest.root ? body : stringifyFrontmatter(body, frontmatter)
|
|
95055
|
+
fileContent: rest.root && Object.keys(frontmatter).length === 0 ? body : stringifyFrontmatter(body, frontmatter)
|
|
92895
95056
|
});
|
|
92896
95057
|
this.frontmatter = frontmatter;
|
|
92897
95058
|
this.body = body;
|
|
92898
95059
|
}
|
|
95060
|
+
/**
|
|
95061
|
+
* Frontmatter written on the root rule. The project `AGENTS.md` and the
|
|
95062
|
+
* global `GEMINI.md` take none; a subclass whose root rule sits inside a
|
|
95063
|
+
* `rules/` directory, where every `.md` needs a `trigger`, overrides it.
|
|
95064
|
+
*/
|
|
95065
|
+
static getRootRuleFrontmatter() {
|
|
95066
|
+
return {};
|
|
95067
|
+
}
|
|
92899
95068
|
static getGlobalRootPath(excludeToolDir) {
|
|
92900
95069
|
return {
|
|
92901
95070
|
relativeDirPath: buildToolPath(ANTIGRAVITY_IDE_GEMINI_DIR, ".", excludeToolDir),
|
|
@@ -92922,13 +95091,16 @@ var AntigravityIdeRule = class extends ToolRule {
|
|
|
92922
95091
|
const paths = this.getSettablePaths({ global });
|
|
92923
95092
|
if (relativeFilePath === paths.root.relativeFilePath && (relativeDirPath === void 0 || relativeDirPath === paths.root.relativeDirPath)) {
|
|
92924
95093
|
const rootPath = paths.root;
|
|
92925
|
-
const
|
|
95094
|
+
const rootFilePath = (0, node_path.join)(outputRoot, rootPath.relativeDirPath, rootPath.relativeFilePath);
|
|
95095
|
+
const rootContent = await readFileContent(rootFilePath);
|
|
95096
|
+
const rootFrontmatter = this.getRootRuleFrontmatter();
|
|
95097
|
+
const rootBody = Object.keys(rootFrontmatter).length === 0 ? rootContent : parseFrontmatter(rootContent, rootFilePath).body;
|
|
92926
95098
|
return new this({
|
|
92927
95099
|
outputRoot,
|
|
92928
95100
|
relativeDirPath: rootPath.relativeDirPath,
|
|
92929
95101
|
relativeFilePath: rootPath.relativeFilePath,
|
|
92930
|
-
frontmatter:
|
|
92931
|
-
body:
|
|
95102
|
+
frontmatter: rootFrontmatter,
|
|
95103
|
+
body: rootBody,
|
|
92932
95104
|
validate,
|
|
92933
95105
|
root: true,
|
|
92934
95106
|
global
|
|
@@ -92988,7 +95160,7 @@ var AntigravityIdeRule = class extends ToolRule {
|
|
|
92988
95160
|
outputRoot,
|
|
92989
95161
|
relativeDirPath: rootPath.relativeDirPath,
|
|
92990
95162
|
relativeFilePath: rootPath.relativeFilePath,
|
|
92991
|
-
frontmatter:
|
|
95163
|
+
frontmatter: this.getRootRuleFrontmatter(),
|
|
92992
95164
|
body: rulesyncRule.getBody(),
|
|
92993
95165
|
validate,
|
|
92994
95166
|
root: true,
|
|
@@ -93019,7 +95191,7 @@ var AntigravityIdeRule = class extends ToolRule {
|
|
|
93019
95191
|
});
|
|
93020
95192
|
}
|
|
93021
95193
|
toRulesyncRule() {
|
|
93022
|
-
if (this.root) return this.toRulesyncRuleDefault();
|
|
95194
|
+
if (this.root) return this.toRulesyncRuleDefault({ body: this.body });
|
|
93023
95195
|
const strategy = STRATEGIES$1.find((s) => s.canHandle(this.frontmatter.trigger));
|
|
93024
95196
|
let rulesyncData = {
|
|
93025
95197
|
globs: [],
|
|
@@ -93097,6 +95269,9 @@ var AntigravityPluginRule = class extends AntigravityIdeRule {
|
|
|
93097
95269
|
static supportsSubprojectRules() {
|
|
93098
95270
|
return false;
|
|
93099
95271
|
}
|
|
95272
|
+
static getRootRuleFrontmatter() {
|
|
95273
|
+
return { trigger: "always_on" };
|
|
95274
|
+
}
|
|
93100
95275
|
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
93101
95276
|
const targets = rulesyncRule.getFrontmatter().targets;
|
|
93102
95277
|
return targets.includes("*") || targets.includes("antigravity-plugin");
|
|
@@ -94686,8 +96861,11 @@ var CodexcliRule = class extends NestedAgentsmdRule {
|
|
|
94686
96861
|
* `.codex/config.toml`, which Codex loads for trusted projects and which
|
|
94687
96862
|
* overrides the 32 KiB default budget for project `AGENTS.md` files.
|
|
94688
96863
|
*
|
|
94689
|
-
* Answers `
|
|
94690
|
-
*
|
|
96864
|
+
* Answers `0` when the key is set to `0`, which Codex treats as "project docs
|
|
96865
|
+
* disabled" (`codex-rs/core/src/agents_md.rs` loads nothing for a zero
|
|
96866
|
+
* budget). Answers `undefined` when the file is missing, is not valid TOML, or
|
|
96867
|
+
* does not set the key to a non-negative integer, so the caller falls back to
|
|
96868
|
+
* the default.
|
|
94691
96869
|
*
|
|
94692
96870
|
* @see https://learn.chatgpt.com/docs/config-file/config-reference
|
|
94693
96871
|
*/
|
|
@@ -94702,7 +96880,7 @@ async function readCodexcliProjectDocMaxBytes(outputRoot) {
|
|
|
94702
96880
|
}
|
|
94703
96881
|
if (!isPlainObject$1(parsed)) return;
|
|
94704
96882
|
const value = parsed.project_doc_max_bytes;
|
|
94705
|
-
return typeof value === "number" && Number.isSafeInteger(value) && value
|
|
96883
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : void 0;
|
|
94706
96884
|
}
|
|
94707
96885
|
//#endregion
|
|
94708
96886
|
//#region src/features/rules/commandcode-rule.ts
|
|
@@ -96064,10 +98242,13 @@ var DevinPluginRule = class extends DevinRule {
|
|
|
96064
98242
|
* Project scope writes the root `AGENTS.md` plus nested `<dir>/AGENTS.md`
|
|
96065
98243
|
* files — the project chain `dsh-agent-instructions` loads from the `.git`
|
|
96066
98244
|
* root down to the session cwd, broad to specific, so a per-directory file
|
|
96067
|
-
* applies only while working under that directory.
|
|
96068
|
-
*
|
|
96069
|
-
*
|
|
96070
|
-
*
|
|
98245
|
+
* applies only while working under that directory. A `localRoot` rule goes to
|
|
98246
|
+
* the project-root `AGENTS.local.md`, the additive local overlay the project
|
|
98247
|
+
* chain loads beside each `AGENTS.md`. Global scope writes the user-global
|
|
98248
|
+
* `~/.dsh/AGENTS.md` (`$DSH_HOME` default), which the harness reads as a single
|
|
98249
|
+
* root file with no `.local.md` overlay, so non-root rules fold into it. The
|
|
98250
|
+
* harness also reads a shared `~/.agents/AGENTS.md` (`$DSH_AGENTS_HOME`) after
|
|
98251
|
+
* it, which rulesync never writes.
|
|
96071
98252
|
*
|
|
96072
98253
|
* @see https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/context/agent-instructions/README.md
|
|
96073
98254
|
*/
|
|
@@ -97738,10 +99919,6 @@ var OmpRule = class OmpRule extends ToolRule {
|
|
|
97738
99919
|
}
|
|
97739
99920
|
};
|
|
97740
99921
|
//#endregion
|
|
97741
|
-
//#region src/constants/openclaw-paths.ts
|
|
97742
|
-
const OPENCLAW_WORKSPACE_DIR_PATH = (0, node_path.join)(".openclaw", "workspace");
|
|
97743
|
-
const OPENCLAW_RULE_FILE_NAME = "AGENTS.md";
|
|
97744
|
-
//#endregion
|
|
97745
99922
|
//#region src/features/rules/openclaw-rule.ts
|
|
97746
99923
|
var OpenclawRule = class OpenclawRule extends ToolRule {
|
|
97747
99924
|
constructor({ fileContent, root, ...rest }) {
|
|
@@ -99879,6 +102056,39 @@ const normalizeRelativeDir = (dir) => node_path.posix.normalize(toPosixPath(dir)
|
|
|
99879
102056
|
/** A tool rule's output path relative to its output root, for comparison. */
|
|
99880
102057
|
const toolOutputPath = (toolRule) => node_path.posix.join(normalizeRelativeDir(toolRule.getRelativeDirPath()), toolRule.getRelativeFilePath());
|
|
99881
102058
|
const formatRulePaths = (rules) => rules.map((r) => (0, node_path.join)(r.getRelativeDirPath(), r.getRelativeFilePath())).join(", ");
|
|
102059
|
+
/**
|
|
102060
|
+
* The leading `---` frontmatter block (with its closing delimiter line) that
|
|
102061
|
+
* every one of `contents` starts with, or `undefined` when one lacks it or the
|
|
102062
|
+
* blocks differ.
|
|
102063
|
+
*/
|
|
102064
|
+
const findSharedLeadingFrontmatter = (contents) => {
|
|
102065
|
+
const blocks = contents.map((content) => /^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/.exec(content)?.[0]);
|
|
102066
|
+
const first = blocks[0];
|
|
102067
|
+
return first !== void 0 && blocks.every((block) => block === first) ? first : void 0;
|
|
102068
|
+
};
|
|
102069
|
+
/**
|
|
102070
|
+
* Whether a group of rules colliding on one output path is composed into one
|
|
102071
|
+
* file, and the frontmatter block the composed file keeps, if any.
|
|
102072
|
+
*/
|
|
102073
|
+
const decideRuleComposition = ({ group, collisionPolicy }) => {
|
|
102074
|
+
const contents = group.map(({ toolRule }) => toolRule.getFileContent());
|
|
102075
|
+
const allGeneratedRulesAreRoots = group.every(({ toolRule }) => toolRule.isRoot());
|
|
102076
|
+
const allFragmentsArePlain = contents.every((content) => !/^---\r?\n/.test(content));
|
|
102077
|
+
const sharedRootFrontmatter = allGeneratedRulesAreRoots ? findSharedLeadingFrontmatter(contents) : void 0;
|
|
102078
|
+
return {
|
|
102079
|
+
shouldCompose: (collisionPolicy === "fold" || collisionPolicy === "compose" || allGeneratedRulesAreRoots) && allFragmentsArePlain || sharedRootFrontmatter !== void 0,
|
|
102080
|
+
sharedRootFrontmatter
|
|
102081
|
+
};
|
|
102082
|
+
};
|
|
102083
|
+
/**
|
|
102084
|
+
* Concatenate colliding rule fragments. With `sharedFrontmatter` (the block
|
|
102085
|
+
* every fragment starts with), the block is stripped from each fragment and
|
|
102086
|
+
* written once at the top.
|
|
102087
|
+
*/
|
|
102088
|
+
const composeRuleFragments = ({ contents, sharedFrontmatter }) => {
|
|
102089
|
+
const merged = contents.map((content) => content.slice(sharedFrontmatter?.length ?? 0).trim()).filter((content) => content.length > 0).join("\n\n");
|
|
102090
|
+
return sharedFrontmatter === void 0 ? merged : `${sharedFrontmatter}${merged}\n`;
|
|
102091
|
+
};
|
|
99882
102092
|
const RulesFeatureOptionsSchema = zod_mini.z.looseObject({
|
|
99883
102093
|
ruleDiscoveryMode: zod_mini.z.optional(zod_mini.z.enum(["none", "explicit"])),
|
|
99884
102094
|
includeLocalRoot: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
@@ -100078,7 +102288,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
|
|
|
100078
102288
|
collisionPolicy: "fold",
|
|
100079
102289
|
projectInstructionBudget: {
|
|
100080
102290
|
bytes: CODEXCLI_PROJECT_DOC_MAX_BYTES,
|
|
100081
|
-
remedy: "Codex CLI stops reading project `AGENTS.md` files once their combined size on the root-to-working-directory chain reaches `project_doc_max_bytes` (32 KiB by default)
|
|
102291
|
+
remedy: "Codex CLI stops reading project `AGENTS.md` files once their combined size on the root-to-working-directory chain reaches `project_doc_max_bytes` (32 KiB by default): it silently truncates the file that crosses the limit and skips the files after it. Move directory-scoped rules into nested `AGENTS.md` files with `agentsmd.subprojectPath` (or `deriveSubprojectPathFromGlobs: true` in rulesync.jsonc), trim the rules, or raise `project_doc_max_bytes` in `.codex/config.toml` (Codex reads that file only if the project is trusted).",
|
|
100082
102292
|
readConfiguredBytes: readCodexcliProjectDocMaxBytes,
|
|
100083
102293
|
configuredSource: "`project_doc_max_bytes` in `.codex/config.toml` (honored only if the project is trusted)",
|
|
100084
102294
|
defaultLimitHint: "Rulesync does not read `~/.codex/config.toml`; ignore this warning if you already raised `project_doc_max_bytes` there."
|
|
@@ -100488,7 +102698,9 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
|
|
|
100488
102698
|
extension: "md",
|
|
100489
102699
|
supportsGlobal: true,
|
|
100490
102700
|
ruleDiscoveryMode: "auto",
|
|
100491
|
-
collisionPolicy: "fold"
|
|
102701
|
+
collisionPolicy: "fold",
|
|
102702
|
+
localRootMode: "separate-local-file",
|
|
102703
|
+
localRootFileName: "AGENTS.local.md"
|
|
100492
102704
|
}
|
|
100493
102705
|
}]
|
|
100494
102706
|
]);
|
|
@@ -100650,14 +102862,22 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
100650
102862
|
* estimate can come out low there. The budget checked is the value set in the
|
|
100651
102863
|
* output root's tool config when the tool supports one (Codex's
|
|
100652
102864
|
* `project_doc_max_bytes` in `.codex/config.toml`), else the tool's default;
|
|
100653
|
-
* the warning names which one it used.
|
|
100654
|
-
*
|
|
100655
|
-
*
|
|
102865
|
+
* the warning names which one it used. A configured budget of `0` disables
|
|
102866
|
+
* project docs in Codex, so that case gets a single warning saying the
|
|
102867
|
+
* generated files will not be loaded instead of a size report. Global scope
|
|
102868
|
+
* is not checked because Codex loads the personal `~/.codex/AGENTS.md`
|
|
102869
|
+
* outside the project budget.
|
|
100656
102870
|
*/
|
|
100657
102871
|
async warnForInstructionBudget({ toolRules, meta }) {
|
|
100658
102872
|
const budget = meta.projectInstructionBudget;
|
|
100659
|
-
if (budget === void 0 || this.global) return;
|
|
102873
|
+
if (budget === void 0 || this.global || toolRules.length === 0) return;
|
|
100660
102874
|
const configuredBytes = await budget.readConfiguredBytes?.(this.outputRoot);
|
|
102875
|
+
const location = this.outputRoot === process.cwd() ? "" : ` under '${this.outputRoot}'`;
|
|
102876
|
+
if (configuredBytes === 0) {
|
|
102877
|
+
const paths = toolRules.map((rule) => toPosixPath((0, node_path.join)(rule.getRelativeDirPath(), rule.getRelativeFilePath())));
|
|
102878
|
+
this.logger.warn(`${budget.configuredSource ?? "The tool config"}${location} is set to 0, which disables ${this.toolTarget}'s project instruction files: the generated ${paths.join(", ")} will not be loaded. Remove the setting or set it to a positive byte count to load them.`);
|
|
102879
|
+
return;
|
|
102880
|
+
}
|
|
100661
102881
|
const limitBytes = configuredBytes ?? budget.bytes;
|
|
100662
102882
|
const limitLabel = budget.readConfiguredBytes === void 0 ? `${limitBytes}-byte limit` : configuredBytes === void 0 ? `${limitBytes}-byte default limit` : `${limitBytes}-byte limit set by ${budget.configuredSource ?? "the tool config"}`;
|
|
100663
102883
|
const remedy = configuredBytes === void 0 && budget.defaultLimitHint !== void 0 ? `${budget.remedy} ${budget.defaultLimitHint}` : budget.remedy;
|
|
@@ -100667,7 +102887,6 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
100667
102887
|
bytes: Buffer.byteLength(addTrailingNewline(rule.getFileContent()), "utf8")
|
|
100668
102888
|
}));
|
|
100669
102889
|
const isAncestorOrSelf = (ancestor, dir) => ancestor === "." || ancestor === dir || dir.startsWith(`${ancestor}/`);
|
|
100670
|
-
const location = this.outputRoot === process.cwd() ? "" : ` under '${this.outputRoot}'`;
|
|
100671
102890
|
const rootFiles = files.filter(({ dir }) => dir === ".");
|
|
100672
102891
|
const rootBytes = rootFiles.reduce((sum, { bytes }) => sum + bytes, 0);
|
|
100673
102892
|
if (rootBytes > limitBytes) {
|
|
@@ -100975,10 +103194,11 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
100975
103194
|
continue;
|
|
100976
103195
|
}
|
|
100977
103196
|
const rootConversion = group.find(({ toolRule }) => toolRule.isRoot());
|
|
100978
|
-
const allGeneratedRulesAreRoots = group.every(({ toolRule }) => toolRule.isRoot());
|
|
100979
103197
|
const hasSourceRoot = group.some(({ rulesyncRule }) => rulesyncRule.getFrontmatter().root === true);
|
|
100980
|
-
const
|
|
100981
|
-
|
|
103198
|
+
const { shouldCompose, sharedRootFrontmatter } = decideRuleComposition({
|
|
103199
|
+
group,
|
|
103200
|
+
collisionPolicy
|
|
103201
|
+
});
|
|
100982
103202
|
if (!shouldCompose && hasSourceRoot) throw new Error(`Multiple generated rules resolve to output path '${path}' for target '${this.toolTarget}', but this target cannot safely compose a collision involving a root rule. Source rules: ${formatRulePaths(group.map(({ rulesyncRule }) => rulesyncRule))}`);
|
|
100983
103203
|
if (!shouldCompose) {
|
|
100984
103204
|
for (const conversion of group) survivors.add(conversion);
|
|
@@ -100986,8 +103206,11 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
100986
103206
|
}
|
|
100987
103207
|
const target = rootConversion ?? group[0];
|
|
100988
103208
|
if (!target) continue;
|
|
100989
|
-
const
|
|
100990
|
-
target.toolRule.setFileContent(
|
|
103209
|
+
const ordered = [target, ...group.filter((rule) => rule !== target)];
|
|
103210
|
+
target.toolRule.setFileContent(composeRuleFragments({
|
|
103211
|
+
contents: ordered.map(({ toolRule }) => toolRule.getFileContent()),
|
|
103212
|
+
sharedFrontmatter: sharedRootFrontmatter
|
|
103213
|
+
}));
|
|
100991
103214
|
survivors.add(target);
|
|
100992
103215
|
}
|
|
100993
103216
|
for (let i = convertedRules.length - 1; i >= 0; i--) {
|
|
@@ -101071,7 +103294,7 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
101071
103294
|
/**
|
|
101072
103295
|
* Handle localRoot rule generation based on tool target.
|
|
101073
103296
|
* - `separate-local-file`: writes a dedicated `*.local.md` root file
|
|
101074
|
-
* (claudecode/legacy: `./CLAUDE.local.md`, rovodev: `./AGENTS.local.md`)
|
|
103297
|
+
* (claudecode/legacy: `./CLAUDE.local.md`, rovodev/dsh: `./AGENTS.local.md`)
|
|
101075
103298
|
* - `append-to-root` (default): appends the body to the root file
|
|
101076
103299
|
*/
|
|
101077
103300
|
handleLocalRootRule(toolRules, localRootRule, factory) {
|
|
@@ -101187,7 +103410,11 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
101187
103410
|
localRoot
|
|
101188
103411
|
});
|
|
101189
103412
|
}
|
|
101190
|
-
const workingRootClass = [
|
|
103413
|
+
const workingRootClass = [
|
|
103414
|
+
CrushRule,
|
|
103415
|
+
ReasonixRule,
|
|
103416
|
+
DshRule
|
|
103417
|
+
].find((base) => isClassOrSubclassOf({
|
|
101191
103418
|
candidate: factory.class,
|
|
101192
103419
|
base
|
|
101193
103420
|
}));
|