devez-vibe 1.7.58 → 1.7.60

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/bin/dvz.exe CHANGED
Binary file
@@ -5,7 +5,7 @@ import { execFile, spawn } from "node:child_process";
5
5
  import { createReadStream, existsSync, readFileSync, readdirSync } from "node:fs";
6
6
  import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
7
7
  import { homedir } from "node:os";
8
- import { dirname, join } from "node:path";
8
+ import { dirname, join, resolve as resolvePath, sep } from "node:path";
9
9
  import { createInterface } from "node:readline";
10
10
  import { fileURLToPath } from "node:url";
11
11
  import {
@@ -722,9 +722,85 @@ function makeOptions(params, sessionId, resume) {
722
722
  else options.sessionId = sessionId;
723
723
  options.canUseTool = (toolName, input, permission) =>
724
724
  requestToolPermission(toolName, input, permission);
725
+ // Hooks run whatever the permission mode, so a read-only role turn holds even
726
+ // under bypass. The policy itself arrives with each prompt.
727
+ options.hooks = {
728
+ PreToolUse: [{ hooks: [(input) => toolPolicyHook(sessionId, input)] }],
729
+ };
725
730
  return options;
726
731
  }
727
732
 
733
+ // A turn under a read-only role (Planner, Reviewer) carries a tool policy:
734
+ // `{ readOnly: true, writableRoots: [...] }`. The role prompt states the same
735
+ // boundary; this keeps it when the model forgets. File tools are judged by the
736
+ // path they touch, shell tools by the shape of the command — a deny names the
737
+ // rule so the model can continue read-only instead of retrying.
738
+ const FILE_WRITING_TOOLS = ["Edit", "MultiEdit", "Write", "NotebookEdit"];
739
+ const SHELL_TOOLS = ["Bash", "PowerShell"];
740
+ const MUTATING_SHELL_PATTERNS = [
741
+ // Any git invocation naming a mutating subcommand, whatever global options
742
+ // (`-C .`, `-c key=value`) sit between `git` and the verb.
743
+ /\bgit\b[^|;&\n]*\b(?:commit|push|pull|merge|rebase|reset|checkout|switch|restore|stash|clean|add|rm|mv|cherry-pick|revert|tag|am|apply|notes|update-ref|symbolic-ref)\b/,
744
+ /\bgit\b[^|;&\n]*\bbranch\s+(?:-[dDmMcCf]|--delete|--move|--copy|--force)\b/,
745
+ /(?:^|[\s;&|(`])(?:rm|rmdir|mv|cp|mkdir|touch|tee|truncate|chmod|chown|ln|dd|patch|install|shred)\s/,
746
+ /\bsed\s+(?:-[a-zA-Z]*i|--in-place)/,
747
+ /\bperl\s+(?:-\S+\s+)*-\S*i\S*\s/,
748
+ /\b(?:npm|pnpm|yarn|bun)\s+(?:i|install|add|remove|rm|uninstall|publish|ci|link|update|up)\b/,
749
+ /\bcargo\s+(?:add|remove|install|publish|clean|fix|update|fmt)\b/,
750
+ /\bpip3?\s+(?:install|uninstall)\b/,
751
+ // A redirect into a file. `2>&1`, `>/dev/null`, `> nul`, `->` and `>=` pass.
752
+ /(?:^|[^<>=-])>{1,2}(?!\s*(?:&|=|\/dev\/null\b|nul\b))/i,
753
+ /\b(?:Set-Content|Add-Content|Out-File|Remove-Item|Move-Item|Copy-Item|New-Item|Rename-Item|Clear-Content|Set-ItemProperty|Export-Csv|Export-Clixml)\b/i,
754
+ ];
755
+
756
+ function pathWithin(cwd, root, target) {
757
+ const normalize = (value) => {
758
+ const resolved = resolvePath(cwd, value).replace(/[\\/]+$/, "");
759
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
760
+ };
761
+ const base = normalize(root);
762
+ const candidate = normalize(target);
763
+ return candidate === base || candidate.startsWith(base + sep) || candidate.startsWith(`${base}/`);
764
+ }
765
+
766
+ // The reason a tool call is denied under the policy, or null when it may run.
767
+ function toolPolicyDecision(policy, cwd, toolName, input) {
768
+ if (!policy || policy.readOnly !== true) return null;
769
+ const roots = Array.isArray(policy.writableRoots) ? policy.writableRoots.filter(Boolean) : [];
770
+ if (FILE_WRITING_TOOLS.includes(toolName)) {
771
+ const target = input?.file_path ?? input?.notebook_path ?? input?.path;
772
+ if (typeof target === "string" && roots.some((root) => pathWithin(cwd, root, target))) return null;
773
+ return roots.length
774
+ ? `이 역할은 읽기 전용입니다. 파일 쓰기는 ${roots.join(", ")} 아래에만 허용되며 요청한 경로는 ${target ?? "(없음)"}입니다. 다른 파일은 고치지 말고 계획에만 적습니다.`
775
+ : `이 역할은 읽기 전용입니다. ${toolName} 도구로 파일을 만들거나 고칠 수 없습니다. 발견한 내용은 보고에 남기고, 수정은 사용자가 역할을 바꾼 뒤 진행합니다.`;
776
+ }
777
+ if (SHELL_TOOLS.includes(toolName)) {
778
+ const command = String(input?.command ?? "");
779
+ if (MUTATING_SHELL_PATTERNS.some((pattern) => pattern.test(command))) {
780
+ return `이 역할은 읽기 전용입니다. 파일이나 저장소 상태를 바꾸는 명령은 실행할 수 없습니다: ${command.slice(0, 160)}. 읽기 명령과 Read·Grep·Glob 도구만 사용하고, 결과를 바꾸는 작업은 보고에 남깁니다.`;
781
+ }
782
+ }
783
+ return null;
784
+ }
785
+
786
+ async function toolPolicyHook(sessionId, input) {
787
+ const session = lookupSession(sessionId);
788
+ const reason = toolPolicyDecision(
789
+ session?.toolPolicy,
790
+ session?.cwd || process.cwd(),
791
+ input?.tool_name,
792
+ input?.tool_input,
793
+ );
794
+ if (!reason) return {};
795
+ return {
796
+ hookSpecificOutput: {
797
+ hookEventName: "PreToolUse",
798
+ permissionDecision: "deny",
799
+ permissionDecisionReason: reason,
800
+ },
801
+ };
802
+ }
803
+
728
804
  async function startAgentQuery(prompt, options) {
729
805
  const warm = await startup({ options });
730
806
  try {
@@ -1258,6 +1334,7 @@ async function createSession(params, resumeId) {
1258
1334
  permissionMode: BOOTSTRAP_PERMISSION_MODE,
1259
1335
  permissionModeVerified: false,
1260
1336
  bypassUnavailable: false,
1337
+ toolPolicy: null,
1261
1338
  models: [],
1262
1339
  queue,
1263
1340
  query: null,
@@ -2711,6 +2788,11 @@ async function runPrompt(session, params) {
2711
2788
  await session.query.applyFlagSettings({ effortLevel: effort });
2712
2789
  }
2713
2790
  session.effort = effort;
2791
+ // The role's read-only policy, or none: it binds this turn and the background
2792
+ // work it starts, until the next prompt replaces it.
2793
+ session.toolPolicy = params.toolPolicy && typeof params.toolPolicy === "object"
2794
+ ? params.toolPolicy
2795
+ : null;
2714
2796
  const content = await inputContent(params.input, params.handoffContext);
2715
2797
  const turnId = beginTurn(session, params.input);
2716
2798
  session.queue.push({
@@ -3453,8 +3535,56 @@ async function runPermissionModeSelfTest() {
3453
3535
  }
3454
3536
  }
3455
3537
 
3538
+ function runToolPolicySelfTest() {
3539
+ const cwd = process.platform === "win32" ? "C:\\repo" : "/repo";
3540
+ const reviewer = { readOnly: true, writableRoots: [] };
3541
+ const planner = { readOnly: true, writableRoots: ["docs/plans"] };
3542
+ const allowed = [
3543
+ [null, "Edit", { file_path: "src/main.rs" }],
3544
+ [{ readOnly: false }, "Bash", { command: "rm -rf target" }],
3545
+ [reviewer, "Read", { file_path: "src/main.rs" }],
3546
+ [reviewer, "Bash", { command: "git diff --stat HEAD~1" }],
3547
+ [reviewer, "Bash", { command: "git log --oneline -5 && git status --porcelain" }],
3548
+ [reviewer, "Bash", { command: "grep -rn \"->\" src | head -20" }],
3549
+ [reviewer, "Bash", { command: "cargo test reviewer 2>&1 | tail -20" }],
3550
+ [reviewer, "Bash", { command: "cargo test > /dev/null 2>&1; echo $?" }],
3551
+ [reviewer, "Bash", { command: "awk '$3 >= 10' report.txt" }],
3552
+ [reviewer, "PowerShell", { command: "Get-Content src/main.rs | Select-Object -First 5" }],
3553
+ [planner, "Write", { file_path: "docs/plans/2026-09-05-x.md" }],
3554
+ [planner, "Write", { file_path: `${cwd}${sep}docs${sep}plans${sep}x.md` }],
3555
+ ];
3556
+ const denied = [
3557
+ [reviewer, "Edit", { file_path: "src/main.rs" }],
3558
+ [reviewer, "Write", { file_path: "docs/plans/x.md" }],
3559
+ [reviewer, "NotebookEdit", { notebook_path: "a.ipynb" }],
3560
+ [planner, "Write", { file_path: "src/main.rs" }],
3561
+ [planner, "Write", { file_path: "docs/plans/../../src/main.rs" }],
3562
+ [planner, "Write", { file_path: "docs/plans-old/x.md" }],
3563
+ [reviewer, "Bash", { command: "git commit -m 'x'" }],
3564
+ [reviewer, "Bash", { command: "git -C . checkout -- src" }],
3565
+ [reviewer, "Bash", { command: "echo hi > notes.txt" }],
3566
+ [reviewer, "Bash", { command: "cat a >> b.log" }],
3567
+ [reviewer, "Bash", { command: "sed -i 's/a/b/' src/main.rs" }],
3568
+ [reviewer, "Bash", { command: "rm -rf target" }],
3569
+ [reviewer, "Bash", { command: "cargo fmt" }],
3570
+ [reviewer, "PowerShell", { command: "Set-Content -Path a.txt -Value x" }],
3571
+ ];
3572
+ for (const [policy, tool, input] of allowed) {
3573
+ const reason = toolPolicyDecision(policy, cwd, tool, input);
3574
+ if (reason !== null) {
3575
+ throw new Error(`Claude tool policy self-test denied ${tool} ${JSON.stringify(input)}: ${reason}`);
3576
+ }
3577
+ }
3578
+ for (const [policy, tool, input] of denied) {
3579
+ if (toolPolicyDecision(policy, cwd, tool, input) === null) {
3580
+ throw new Error(`Claude tool policy self-test allowed ${tool} ${JSON.stringify(input)}`);
3581
+ }
3582
+ }
3583
+ }
3584
+
3456
3585
  async function runSelfTest() {
3457
3586
  await runPermissionModeSelfTest();
3587
+ runToolPolicySelfTest();
3458
3588
  const equivalentCwd = process.platform === "win32"
3459
3589
  ? sameCwd("D:\\Repo", "d:/repo/")
3460
3590
  : sameCwd("/tmp/repo", "/tmp/repo/");
package/package.json CHANGED
@@ -1,48 +1,48 @@
1
- {
2
- "name": "devez-vibe",
3
- "version": "1.7.58",
4
- "description": "Stable terminal UI for Codex and Claude Agent SDK",
5
- "keywords": [
6
- "codex",
7
- "cli",
8
- "tui",
9
- "terminal",
10
- "app-server",
11
- "claude-agent-sdk"
12
- ],
13
- "homepage": "https://github.com/MrHoje/Devez-vibe#readme",
14
- "bugs": "https://github.com/MrHoje/Devez-vibe/issues",
15
- "repository": {
16
- "type": "git",
17
- "url": "git+https://github.com/MrHoje/Devez-vibe.git"
18
- },
19
- "license": "MIT",
20
- "bin": {
21
- "dvz": "bin/dvz.exe"
22
- },
23
- "files": [
24
- "bin/dvz.exe",
25
- "bridge/claude-agent-sdk-bridge.mjs",
26
- "install-skills.mjs",
27
- "prepare-search-deps.mjs",
28
- "skills/luna-loop/",
29
- "skills/insane-search/",
30
- "README.md",
31
- "LICENSE"
32
- ],
33
- "os": [
34
- "win32"
35
- ],
36
- "cpu": [
37
- "x64"
38
- ],
39
- "engines": {
40
- "node": ">=18"
41
- },
42
- "scripts": {
43
- "postinstall": "node install-skills.mjs && node prepare-search-deps.mjs"
44
- },
45
- "dependencies": {
46
- "@anthropic-ai/claude-agent-sdk": "0.3.260"
47
- }
48
- }
1
+ {
2
+ "name": "devez-vibe",
3
+ "version": "1.7.60",
4
+ "description": "Stable terminal UI for Codex and Claude Agent SDK",
5
+ "keywords": [
6
+ "codex",
7
+ "cli",
8
+ "tui",
9
+ "terminal",
10
+ "app-server",
11
+ "claude-agent-sdk"
12
+ ],
13
+ "homepage": "https://github.com/MrHoje/Devez-vibe#readme",
14
+ "bugs": "https://github.com/MrHoje/Devez-vibe/issues",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/MrHoje/Devez-vibe.git"
18
+ },
19
+ "license": "MIT",
20
+ "bin": {
21
+ "dvz": "bin/dvz.exe"
22
+ },
23
+ "files": [
24
+ "bin/dvz.exe",
25
+ "bridge/claude-agent-sdk-bridge.mjs",
26
+ "install-skills.mjs",
27
+ "prepare-search-deps.mjs",
28
+ "skills/luna-loop/",
29
+ "skills/insane-search/",
30
+ "README.md",
31
+ "LICENSE"
32
+ ],
33
+ "os": [
34
+ "win32"
35
+ ],
36
+ "cpu": [
37
+ "x64"
38
+ ],
39
+ "engines": {
40
+ "node": ">=18"
41
+ },
42
+ "scripts": {
43
+ "postinstall": "node install-skills.mjs && node prepare-search-deps.mjs"
44
+ },
45
+ "dependencies": {
46
+ "@anthropic-ai/claude-agent-sdk": "0.3.260"
47
+ }
48
+ }