Skip to content

Commit e51bb73

Browse files
authored
Merge pull request #2042 from dyoshikawa/resolve-scrap-issue-1886-takt-permissions
feat(takt): add mode-based permissions adapter
2 parents b7788da + 3f4d499 commit e51bb73

12 files changed

Lines changed: 665 additions & 3 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ The tables below show whether each tool supports a given feature (✅ = supporte
9595
| Kilo Code |||||||||
9696
| Roo Code ||||||| | |
9797
| Rovodev (Atlassian) || || ||| ||
98-
| Takt || | |||| | |
98+
| Takt || | |||| | |
9999
| Vibe Code |||| |||||
100100
| Qwen Code |||||||||
101101
| Reasonix | | || | | | | |

docs/reference/file-formats.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -883,4 +883,6 @@ For the Grok Build CLI (`grokcli`), this generates the `[ui] permission_mode` to
883883

884884
For Vibe (mistral-vibe), this generates per-tool `[tools.<tool>]` tables in the shared `.vibe/config.toml` (project mode) or `~/.vibe/config.toml` (global mode). Tool-name mapping: `bash` → `bash`, `read` → `read_file`, `edit`/`write` → `write_file`, `webfetch` → `fetch`, `websearch` → `search_web`. Within a category, the catch-all `*` pattern sets the per-tool `permission` (`allow` → `always`, `ask` → `ask`) and also toggles the top-level `enabled_tools` / `disabled_tools` filters; specific patterns become **`allowlist` / `denylist`** entries — these are the keys Vibe's permission engine actually reads (`BaseToolConfig`), so the legacy `allow` / `deny` keys are dropped on generate (still honored as a fallback on import). Vibe has no per-pattern `ask`, so pattern-level `ask` rules are skipped with a warning. The `config.toml` file is shared with the MCP and hooks features, so writes merge non-destructively and the file is never deleted. See [mistral-vibe](https://github.com/mistralai/mistral-vibe) (`vibe/core/tools/base.py`).
885885

886+
For Takt, this generates the `default_permission_mode` under `provider_profiles.<provider>` in the shared `.takt/config.yaml` (project mode) or `~/.takt/config.yaml` (global mode). Takt does not have per-tool / per-pattern rules; tool gating is a single coarse mode per provider profile, ordered `readonly` < `edit` < `full` (`readonly` may only read, `edit` may also edit/write files, `full` may also run shell commands). The active provider is named by the top-level `provider:` key (defaulting to `claude`). The mapping is therefore **lossy**: on generate, a single mode is derived with this precedence — (1) any `deny` rule anywhere ⇒ `readonly` (conservative — keep the narrowest mode whenever the user expressed any restriction); (2) else any `edit`/`write` category `allow` rule ⇒ `edit`; (3) else any `bash` category `allow` rule ⇒ `full`; (4) else ⇒ `readonly` (safe default). On import, `full` ⇄ `bash: { "*": "allow" }`, `edit` ⇄ `edit: { "*": "allow" }`, and `readonly` (or an unset/unknown mode) ⇄ `bash: { "*": "deny" }`. `config.yaml` is shared with other Takt settings, so the mode is merged in place — the active provider's other keys (e.g. `step_permission_overrides`), every other provider profile, and all other top-level keys are preserved — and the file is never deleted. See the [Takt configuration docs](https://github.com/nrslib/takt/blob/main/docs/configuration.md).
887+
886888
> **Note: Interaction with ignore feature.** Both the ignore feature and the permissions feature can manage `Read` tool deny entries in `.claude/settings.json`. When both features configure the `Read` tool, the **permissions feature takes precedence** and a warning is emitted. If you only need to restrict file reads based on glob patterns, use the ignore feature (`.rulesync/.aiignore`). Use permissions only when you need fine-grained `allow`/`ask`/`deny` control over the `Read` tool.

docs/reference/supported-tools.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ Rulesync supports both **generation** and **import** for All of the major AI cod
2525
| Kilo Code | kilo | ✅ 🌏 || ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 |
2626
| Roo Code | roo | ✅ 🌏 ||||| ✅ 🌏 | | |
2727
| Rovodev (Atlassian) | rovodev | ✅ 🌏 | | 🌏 | | ✅ 🌏 | ✅ 🌏 | | 🌏 |
28-
| Takt | takt | ✅ 🌏 | | | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | | |
28+
| Takt | takt | ✅ 🌏 | | | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | | ✅ 🌏 |
2929
| Vibe Code | vibe | ✅ 🌏 || ✅ 🌏 | | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 |
3030
| Qwen Code | qwencode | ✅ 🌏 || ✅ 🌏 🔧 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 |
3131
| Reasonix | reasonix | | | ✅ 🌏 | | | | | |

skills/rulesync/file-formats.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -883,4 +883,6 @@ For the Grok Build CLI (`grokcli`), this generates the `[ui] permission_mode` to
883883

884884
For Vibe (mistral-vibe), this generates per-tool `[tools.<tool>]` tables in the shared `.vibe/config.toml` (project mode) or `~/.vibe/config.toml` (global mode). Tool-name mapping: `bash` → `bash`, `read` → `read_file`, `edit`/`write` → `write_file`, `webfetch` → `fetch`, `websearch` → `search_web`. Within a category, the catch-all `*` pattern sets the per-tool `permission` (`allow` → `always`, `ask` → `ask`) and also toggles the top-level `enabled_tools` / `disabled_tools` filters; specific patterns become **`allowlist` / `denylist`** entries — these are the keys Vibe's permission engine actually reads (`BaseToolConfig`), so the legacy `allow` / `deny` keys are dropped on generate (still honored as a fallback on import). Vibe has no per-pattern `ask`, so pattern-level `ask` rules are skipped with a warning. The `config.toml` file is shared with the MCP and hooks features, so writes merge non-destructively and the file is never deleted. See [mistral-vibe](https://github.com/mistralai/mistral-vibe) (`vibe/core/tools/base.py`).
885885

886+
For Takt, this generates the `default_permission_mode` under `provider_profiles.<provider>` in the shared `.takt/config.yaml` (project mode) or `~/.takt/config.yaml` (global mode). Takt does not have per-tool / per-pattern rules; tool gating is a single coarse mode per provider profile, ordered `readonly` < `edit` < `full` (`readonly` may only read, `edit` may also edit/write files, `full` may also run shell commands). The active provider is named by the top-level `provider:` key (defaulting to `claude`). The mapping is therefore **lossy**: on generate, a single mode is derived with this precedence — (1) any `deny` rule anywhere ⇒ `readonly` (conservative — keep the narrowest mode whenever the user expressed any restriction); (2) else any `edit`/`write` category `allow` rule ⇒ `edit`; (3) else any `bash` category `allow` rule ⇒ `full`; (4) else ⇒ `readonly` (safe default). On import, `full` ⇄ `bash: { "*": "allow" }`, `edit` ⇄ `edit: { "*": "allow" }`, and `readonly` (or an unset/unknown mode) ⇄ `bash: { "*": "deny" }`. `config.yaml` is shared with other Takt settings, so the mode is merged in place — the active provider's other keys (e.g. `step_permission_overrides`), every other provider profile, and all other top-level keys are preserved — and the file is never deleted. See the [Takt configuration docs](https://github.com/nrslib/takt/blob/main/docs/configuration.md).
887+
886888
> **Note: Interaction with ignore feature.** Both the ignore feature and the permissions feature can manage `Read` tool deny entries in `.claude/settings.json`. When both features configure the `Read` tool, the **permissions feature takes precedence** and a warning is emitted. If you only need to restrict file reads based on glob patterns, use the ignore feature (`.rulesync/.aiignore`). Use permissions only when you need fine-grained `allow`/`ask`/`deny` control over the `Read` tool.

skills/rulesync/supported-tools.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ Rulesync supports both **generation** and **import** for All of the major AI cod
2525
| Kilo Code | kilo | ✅ 🌏 || ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 |
2626
| Roo Code | roo | ✅ 🌏 ||||| ✅ 🌏 | | |
2727
| Rovodev (Atlassian) | rovodev | ✅ 🌏 | | 🌏 | | ✅ 🌏 | ✅ 🌏 | | 🌏 |
28-
| Takt | takt | ✅ 🌏 | | | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | | |
28+
| Takt | takt | ✅ 🌏 | | | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | | ✅ 🌏 |
2929
| Vibe Code | vibe | ✅ 🌏 || ✅ 🌏 | | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 |
3030
| Qwen Code | qwencode | ✅ 🌏 || ✅ 🌏 🔧 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 |
3131
| Reasonix | reasonix | | | ✅ 🌏 | | | | | |

src/constants/takt-paths.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,11 @@ export const TAKT_SKILLS_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "knowledge");
99
export const TAKT_SUBAGENTS_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "personas");
1010
export const TAKT_OUTPUT_CONTRACTS_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "output-contracts");
1111
export const TAKT_RULE_OVERVIEW_FILE_NAME = "overview.md";
12+
13+
/**
14+
* Takt's shared config file. Lives at `.takt/config.yaml` (project) and
15+
* `~/.takt/config.yaml` (global); it holds the active provider, provider
16+
* profiles (including permission modes), and other Takt settings.
17+
* @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
18+
*/
19+
export const TAKT_CONFIG_FILE_NAME = "config.yaml";

src/e2e/e2e-permissions.spec.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,61 @@ describe("E2E: permissions", () => {
471471
expect(parsed.disabled_tools).toContain("write_file");
472472
});
473473

474+
it("should generate takt permissions into .takt/config.yaml", async () => {
475+
const testDir = getTestDir();
476+
477+
// Pre-seed config.yaml with an active provider and unrelated keys to verify
478+
// the non-destructive merge and that the mode is written under the active
479+
// provider profile.
480+
await writeFileContent(
481+
join(testDir, ".takt", "config.yaml"),
482+
["provider: codex", "model: gpt-5", "provider_profiles:", " codex: {}"].join("\n"),
483+
);
484+
await writeFileContent(
485+
join(testDir, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH),
486+
JSON.stringify(
487+
{
488+
permission: {
489+
bash: { "*": "allow" },
490+
},
491+
},
492+
null,
493+
2,
494+
),
495+
);
496+
497+
await runGenerate({ target: "takt", features: "permissions" });
498+
499+
const parsed = toTable(load(await readFileContent(join(testDir, ".takt", "config.yaml"))));
500+
// Active provider preserved; only-bash allow collapses to the `full` mode.
501+
expect(parsed.provider).toBe("codex");
502+
expect(parsed.model).toBe("gpt-5");
503+
const profiles = toTable(parsed.provider_profiles);
504+
expect(toTable(profiles.codex).default_permission_mode).toBe("full");
505+
});
506+
507+
it("should import takt permissions into .rulesync/permissions.json", async () => {
508+
const testDir = getTestDir();
509+
510+
await writeFileContent(
511+
join(testDir, ".takt", "config.yaml"),
512+
[
513+
"provider: claude",
514+
"provider_profiles:",
515+
" claude:",
516+
" default_permission_mode: edit",
517+
].join("\n"),
518+
);
519+
520+
await runImport({ target: "takt", features: "permissions" });
521+
522+
const content = JSON.parse(
523+
await readFileContent(join(testDir, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH)),
524+
);
525+
// `edit` mode imports back to an `edit` allow catch-all.
526+
expect(content.permission.edit["*"]).toBe("allow");
527+
});
528+
474529
it("should remove denied Kiro web tools from existing allowedTools", async () => {
475530
const testDir = getTestDir();
476531

@@ -1507,6 +1562,47 @@ describe("E2E: permissions (global mode)", () => {
15071562
expect(content).toContain("[mcp_servers.example]");
15081563
expect(content).toContain('theme = "dark"');
15091564
});
1565+
1566+
it("should generate takt permissions in home directory with --global", async () => {
1567+
const projectDir = getProjectDir();
1568+
const homeDir = getHomeDir();
1569+
1570+
await writeFileContent(
1571+
join(projectDir, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH),
1572+
JSON.stringify(
1573+
{
1574+
permission: {
1575+
bash: { "*": "allow", "rm *": "deny" },
1576+
},
1577+
},
1578+
null,
1579+
2,
1580+
),
1581+
);
1582+
1583+
// Pre-seed config.yaml with unrelated user settings to verify the
1584+
// non-destructive merge into ~/.takt/config.yaml.
1585+
await writeFileContent(
1586+
join(homeDir, ".takt", "config.yaml"),
1587+
["provider: claude", "model: claude-sonnet"].join("\n"),
1588+
);
1589+
1590+
await runGenerate({
1591+
target: "takt",
1592+
features: "permissions",
1593+
global: true,
1594+
env: { HOME_DIR: homeDir },
1595+
});
1596+
1597+
// Takt gates tools with the coarse `default_permission_mode` under
1598+
// `provider_profiles.<provider>` in the global ~/.takt/config.yaml. A `deny`
1599+
// rule collapses the lossy mapping to `readonly`.
1600+
const parsed = toTable(load(await readFileContent(join(homeDir, ".takt", "config.yaml"))));
1601+
const profiles = toTable(parsed.provider_profiles);
1602+
expect(toTable(profiles.claude).default_permission_mode).toBe("readonly");
1603+
// Unrelated user settings preserved by the non-destructive merge.
1604+
expect(parsed.model).toBe("claude-sonnet");
1605+
});
15101606
});
15111607

15121608
type AugmentEntry = {

src/features/permissions/permissions-processor.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ describe("PermissionsProcessor", () => {
9494
"kiro-ide",
9595
"opencode",
9696
"qwencode",
97+
"takt",
9798
"vibe",
9899
"zed",
99100
]);
@@ -116,6 +117,7 @@ describe("PermissionsProcessor", () => {
116117
"opencode",
117118
"qwencode",
118119
"rovodev",
120+
"takt",
119121
"vibe",
120122
"warp",
121123
"zed",
@@ -140,6 +142,7 @@ describe("PermissionsProcessor", () => {
140142
"kiro-ide",
141143
"opencode",
142144
"qwencode",
145+
"takt",
143146
"vibe",
144147
"zed",
145148
]);

src/features/permissions/permissions-processor.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { OpencodePermissions } from "./opencode-permissions.js";
2626
import { QwencodePermissions } from "./qwencode-permissions.js";
2727
import { RovodevPermissions } from "./rovodev-permissions.js";
2828
import { RulesyncPermissions } from "./rulesync-permissions.js";
29+
import { TaktPermissions } from "./takt-permissions.js";
2930
import type {
3031
ToolPermissionsForDeletionParams,
3132
ToolPermissionsFromFileParams,
@@ -295,6 +296,21 @@ export const toolPermissionsFactories = new Map<
295296
},
296297
},
297298
],
299+
[
300+
"takt",
301+
{
302+
class: TaktPermissions,
303+
meta: {
304+
// Takt gates tools with the coarse `default_permission_mode`
305+
// (readonly < edit < full) under `provider_profiles.<provider>` in the
306+
// shared config: `.takt/config.yaml` (project) and
307+
// `~/.takt/config.yaml` (global).
308+
supportsProject: true,
309+
supportsGlobal: true,
310+
supportsImport: true,
311+
},
312+
},
313+
],
298314
[
299315
"vibe",
300316
{

0 commit comments

Comments
 (0)