Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/reference/file-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -921,6 +921,8 @@ For Kiro, this generates tool permission settings in `.kiro/agents/default.json`
- `webfetch` / `websearch` with pattern `*` map to `allowedTools` entries (`web_fetch` / `web_search`)
- `ask` rules are skipped with a warning (Kiro config does not support explicit ask entries)

> **Kiro-only override (`kiro` key):** Kiro's agent config exposes per-tool `toolsSettings` knobs with no canonical allow/ask/deny category. Author them through a tool-scoped `kiro` override under `toolsSettings`: the shell auto-trust flags `shell.autoAllowReadonly` / `shell.denyByDefault`, the `aws` built-in tool's `allowedServices` / `deniedServices` (+ `autoAllowReadonly`), and the `web_fetch` domain trust arrays `trusted` / `blocked` (regex host patterns; Kiro documents these for `web_fetch` only — `web_search` has no domain-trust surface). Example: `{ "permission": { … }, "kiro": { "toolsSettings": { "shell": { "autoAllowReadonly": true }, "aws": { "allowedServices": ["s3"], "deniedServices": ["eks"] }, "web_fetch": { "trusted": [".*github\\.com.*"] } } } }`. The override is **deep-merged per `toolsSettings` key** (the override wins at the leaf) so authoring `shell.autoAllowReadonly` keeps the canonical-generated `shell.allowedCommands`; the shared `permission` block keeps driving `shell.{allowed,denied}Commands`, `read`/`write`/`grep`/`glob` paths, and the `web_fetch`/`web_search` `allowedTools` toggles. Existing non-canonical `shell` flags are preserved across regenerate even without an override. On **import**, these Kiro-specific surfaces are lifted into the `kiro` override so they round-trip. It is a `looseObject` at every level, so future Kiro `toolsSettings` fields pass through verbatim. **Out of scope:** Kiro's MCP `autoApprove` / `disabledTools` lists are **not** modeled here — they live in a separate file (`.kiro/settings/mcp.json`, under `mcpServers.<name>`), not the agent config this translator writes, and reconciling them with the canonical `mcp__*` model is a distinct design question. See the [Kiro built-in tools](https://kiro.dev/docs/cli/reference/built-in-tools/) and [configuration reference](https://kiro.dev/docs/cli/custom-agents/configuration-reference/) docs.

For Cursor CLI, this generates `permissions` entries in `.cursor/cli.json` (project mode) or `~/.cursor/cli-config.json` (global mode). Cursor CLI only supports `allow` and `deny` decisions, so `ask` rules are skipped with a warning. Tool categories are mapped to PascalCase Cursor tool names (`bash` → `Shell`, `read` → `Read`, `edit`/`write` → `Write`, `webfetch` → `WebFetch`, `mcp__*` → `Mcp`). Existing Cursor-specific entries that Rulesync does not manage (for example, MCP entries with extra fields) are preserved on round-trip.

> **Cursor-only override (`cursor` key):** Cursor's `cli.json` carries scalar autonomy settings with no canonical permission category — `approvalMode` (`allowlist` | `auto-review` | `unrestricted`) and a `sandbox` object (`mode`/`networkAccess`). Add a tool-scoped `cursor` override to author them: its fields are merged into the top level of `cli.json` while the shared `permission` block keeps driving the `permissions.allow`/`permissions.deny` arrays (the override cannot clobber that managed block). On import, `approvalMode` and `sandbox` round-trip back into the `cursor` override. It is a `looseObject`, so `sandbox`'s (currently undocumented) value set passes through verbatim and extra `cli.json` keys can be authored here (they are merged verbatim on generate); note that only `approvalMode` and `sandbox` are re-extracted on import.
Expand Down
2 changes: 2 additions & 0 deletions skills/rulesync/file-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -921,6 +921,8 @@ For Kiro, this generates tool permission settings in `.kiro/agents/default.json`
- `webfetch` / `websearch` with pattern `*` map to `allowedTools` entries (`web_fetch` / `web_search`)
- `ask` rules are skipped with a warning (Kiro config does not support explicit ask entries)

> **Kiro-only override (`kiro` key):** Kiro's agent config exposes per-tool `toolsSettings` knobs with no canonical allow/ask/deny category. Author them through a tool-scoped `kiro` override under `toolsSettings`: the shell auto-trust flags `shell.autoAllowReadonly` / `shell.denyByDefault`, the `aws` built-in tool's `allowedServices` / `deniedServices` (+ `autoAllowReadonly`), and the `web_fetch` domain trust arrays `trusted` / `blocked` (regex host patterns; Kiro documents these for `web_fetch` only — `web_search` has no domain-trust surface). Example: `{ "permission": { … }, "kiro": { "toolsSettings": { "shell": { "autoAllowReadonly": true }, "aws": { "allowedServices": ["s3"], "deniedServices": ["eks"] }, "web_fetch": { "trusted": [".*github\\.com.*"] } } } }`. The override is **deep-merged per `toolsSettings` key** (the override wins at the leaf) so authoring `shell.autoAllowReadonly` keeps the canonical-generated `shell.allowedCommands`; the shared `permission` block keeps driving `shell.{allowed,denied}Commands`, `read`/`write`/`grep`/`glob` paths, and the `web_fetch`/`web_search` `allowedTools` toggles. Existing non-canonical `shell` flags are preserved across regenerate even without an override. On **import**, these Kiro-specific surfaces are lifted into the `kiro` override so they round-trip. It is a `looseObject` at every level, so future Kiro `toolsSettings` fields pass through verbatim. **Out of scope:** Kiro's MCP `autoApprove` / `disabledTools` lists are **not** modeled here — they live in a separate file (`.kiro/settings/mcp.json`, under `mcpServers.<name>`), not the agent config this translator writes, and reconciling them with the canonical `mcp__*` model is a distinct design question. See the [Kiro built-in tools](https://kiro.dev/docs/cli/reference/built-in-tools/) and [configuration reference](https://kiro.dev/docs/cli/custom-agents/configuration-reference/) docs.

For Cursor CLI, this generates `permissions` entries in `.cursor/cli.json` (project mode) or `~/.cursor/cli-config.json` (global mode). Cursor CLI only supports `allow` and `deny` decisions, so `ask` rules are skipped with a warning. Tool categories are mapped to PascalCase Cursor tool names (`bash` → `Shell`, `read` → `Read`, `edit`/`write` → `Write`, `webfetch` → `WebFetch`, `mcp__*` → `Mcp`). Existing Cursor-specific entries that Rulesync does not manage (for example, MCP entries with extra fields) are preserved on round-trip.

> **Cursor-only override (`cursor` key):** Cursor's `cli.json` carries scalar autonomy settings with no canonical permission category — `approvalMode` (`allowlist` | `auto-review` | `unrestricted`) and a `sandbox` object (`mode`/`networkAccess`). Add a tool-scoped `cursor` override to author them: its fields are merged into the top level of `cli.json` while the shared `permission` block keeps driving the `permissions.allow`/`permissions.deny` arrays (the override cannot clobber that managed block). On import, `approvalMode` and `sandbox` round-trip back into the `cursor` override. It is a `looseObject`, so `sandbox`'s (currently undocumented) value set passes through verbatim and extra `cli.json` keys can be authored here (they are merged verbatim on generate); note that only `approvalMode` and `sandbox` are re-extracted on import.
Expand Down
176 changes: 176 additions & 0 deletions src/features/permissions/kiro-permissions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,182 @@ describe("KiroPermissions", () => {
expect(JSON.parse(loaded.getFileContent()).model).toBe("x");
});

it("should author kiro-scoped toolsSettings (shell flags, aws, web_fetch) from the override", async () => {
const rulesyncPermissions = new RulesyncPermissions({
outputRoot: testDir,
relativeDirPath: ".rulesync",
relativeFilePath: "permissions.json",
fileContent: JSON.stringify({
permission: { bash: { "git *": "allow" } },
kiro: {
toolsSettings: {
shell: { autoAllowReadonly: true, denyByDefault: false },
aws: { allowedServices: ["s3"], deniedServices: ["eks"] },
web_fetch: { trusted: [".*github\\.com.*"], blocked: [".*blocked\\.example\\.com.*"] },
},
},
}),
});

const kiroPermissions = await KiroPermissions.fromRulesyncPermissions({
outputRoot: testDir,
rulesyncPermissions,
});

const content = JSON.parse(kiroPermissions.getFileContent());
// The override's shell flags merge WITH the canonical-generated command list.
expect(content.toolsSettings.shell.allowedCommands).toContain("git *");
expect(content.toolsSettings.shell.autoAllowReadonly).toBe(true);
expect(content.toolsSettings.shell.denyByDefault).toBe(false);
expect(content.toolsSettings.aws).toEqual({
allowedServices: ["s3"],
deniedServices: ["eks"],
});
expect(content.toolsSettings.web_fetch).toEqual({
trusted: [".*github\\.com.*"],
blocked: [".*blocked\\.example\\.com.*"],
});
});

it("must NOT let the kiro override clobber canonical deny lists", async () => {
// Guard: the override is for non-canonical knobs only. An attempt to author
// shell.deniedCommands / read.deniedPaths through the override must be ignored so a
// canonical-generated deny cannot be silently weakened.
const rulesyncPermissions = new RulesyncPermissions({
outputRoot: testDir,
relativeDirPath: ".rulesync",
relativeFilePath: "permissions.json",
fileContent: JSON.stringify({
permission: {
bash: { "rm *": "deny" },
read: { ".env": "deny" },
},
kiro: {
toolsSettings: {
shell: { deniedCommands: [], autoAllowReadonly: true },
read: { deniedPaths: [] },
},
},
}),
});

const kiroPermissions = await KiroPermissions.fromRulesyncPermissions({
outputRoot: testDir,
rulesyncPermissions,
});

const content = JSON.parse(kiroPermissions.getFileContent());
// Canonical denies survive; the override's clobbering leaves/keys are ignored.
expect(content.toolsSettings.shell.deniedCommands).toEqual(["rm *"]);
expect(content.toolsSettings.read.deniedPaths).toEqual([".env"]);
// ...but the legitimate non-canonical shell flag still applies.
expect(content.toolsSettings.shell.autoAllowReadonly).toBe(true);
});

it("should extract kiro-specific toolsSettings into the kiro override on import", () => {
const kiroPermissions = new KiroPermissions({
outputRoot: testDir,
relativeDirPath: join(".kiro", "agents"),
relativeFilePath: "default.json",
fileContent: JSON.stringify({
toolsSettings: {
shell: {
allowedCommands: ["git *"],
deniedCommands: ["rm *"],
autoAllowReadonly: true,
denyByDefault: true,
},
aws: { allowedServices: ["s3"], deniedServices: ["eks"] },
web_fetch: { trusted: [".*github\\.com.*"] },
},
}),
});

const json = kiroPermissions.toRulesyncPermissions().getJson();
// Canonical command lists still drive the bash category.
expect(json.permission.bash).toEqual({ "git *": "allow", "rm *": "deny" });
// Non-canonical shell flags + aws + web_fetch round-trip through the override.
expect(json.kiro?.toolsSettings).toEqual({
shell: { autoAllowReadonly: true, denyByDefault: true },
aws: { allowedServices: ["s3"], deniedServices: ["eks"] },
web_fetch: { trusted: [".*github\\.com.*"] },
});
});

it("should omit the kiro override when no kiro-specific toolsSettings are present", () => {
const kiroPermissions = new KiroPermissions({
outputRoot: testDir,
relativeDirPath: join(".kiro", "agents"),
relativeFilePath: "default.json",
fileContent: JSON.stringify({
toolsSettings: { shell: { allowedCommands: ["git *"] } },
}),
});

const json = kiroPermissions.toRulesyncPermissions().getJson();
expect(json.permission.bash).toEqual({ "git *": "allow" });
expect(json.kiro).toBeUndefined();
});

it("should preserve existing shell auto-trust flags across regenerate without an override", async () => {
const kiroDir = join(testDir, ".kiro", "agents");
await ensureDir(kiroDir);
await writeFileContent(
join(kiroDir, "default.json"),
JSON.stringify({
toolsSettings: { shell: { allowedCommands: ["old"], autoAllowReadonly: true } },
}),
);

const rulesyncPermissions = new RulesyncPermissions({
outputRoot: testDir,
relativeDirPath: ".rulesync",
relativeFilePath: "permissions.json",
fileContent: JSON.stringify({ permission: { bash: { "git *": "allow" } } }),
});

const kiroPermissions = await KiroPermissions.fromRulesyncPermissions({
outputRoot: testDir,
rulesyncPermissions,
});

const content = JSON.parse(kiroPermissions.getFileContent());
// The canonical list is regenerated, but the hand-set flag is not dropped.
expect(content.toolsSettings.shell.allowedCommands).toEqual(["git *"]);
expect(content.toolsSettings.shell.autoAllowReadonly).toBe(true);
});

it("should round-trip kiro override: import then re-generate keeps the specific surfaces", async () => {
const source = new KiroPermissions({
outputRoot: testDir,
relativeDirPath: join(".kiro", "agents"),
relativeFilePath: "default.json",
fileContent: JSON.stringify({
toolsSettings: {
shell: { allowedCommands: ["git *"], denyByDefault: true },
aws: { deniedServices: ["eks"] },
},
}),
});
const imported = source.toRulesyncPermissions().getJson();

const rulesyncPermissions = new RulesyncPermissions({
outputRoot: testDir,
relativeDirPath: ".rulesync",
relativeFilePath: "permissions.json",
fileContent: JSON.stringify(imported),
});
const regenerated = await KiroPermissions.fromRulesyncPermissions({
outputRoot: testDir,
rulesyncPermissions,
});

const content = JSON.parse(regenerated.getFileContent());
expect(content.toolsSettings.shell.allowedCommands).toEqual(["git *"]);
expect(content.toolsSettings.shell.denyByDefault).toBe(true);
expect(content.toolsSettings.aws).toEqual({ deniedServices: ["eks"] });
});

it("should remove web tools from allowedTools when denied", async () => {
const kiroDir = join(testDir, ".kiro", "agents");
await ensureDir(kiroDir);
Expand Down
Loading
Loading